Skip to content

Commit

Permalink
Merge branch 'beta' into alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
do4gr committed Feb 1, 2019
2 parents 4ebe0c8 + 76e6155 commit 496fb21
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.prisma.deploy.migration.inference
import com.prisma.deploy.connector.{InferredTables, MissingBackRelations}
import com.prisma.deploy.migration.DirectiveTypes.{MongoInlineRelationDirective, PGInlineRelationDirective, RelationTableDirective}
import com.prisma.deploy.migration.validation._
import com.prisma.deploy.schema.InvalidRelationName
import com.prisma.deploy.schema.RelationNameNeeded
import com.prisma.deploy.validation.NameConstraints
import com.prisma.shared.models.ConnectorCapability.{LegacyDataModelCapability, MigrationsCapability, RelationLinkListCapability}
import com.prisma.shared.models.FieldBehaviour.{IdBehaviour, IdStrategy}
Expand Down Expand Up @@ -231,23 +231,23 @@ case class SchemaInferrerImpl(
def concat(modelName: String, otherModelName: String): String = {
val concatenatedString = s"${modelName}To${otherModelName}"

!NameConstraints.isValidRelationName(concatenatedString) match {
case true if otherModelName.length > modelName.length => concat(modelName, otherModelName.substring(0, otherModelName.length - 1))
case true => concat(modelName.substring(0, modelName.length - 1), otherModelName)
case false => concatenatedString
NameConstraints.isValidRelationName(concatenatedString) match {
case false if otherModelName.length > modelName.length => concat(modelName, otherModelName.substring(0, otherModelName.length - 1))
case false => concat(modelName.substring(0, modelName.length - 1), otherModelName)
case true => concatenatedString
}
}
concat(modelA, modelB)
}

val relationNameOnRelatedField: Option[String] = relatedField.flatMap(_.relationName)

val relationName = (relationField.relationName, relationNameOnRelatedField) match {
case (Some(name), _) if !NameConstraints.isValidRelationName(name) => throw InvalidRelationName(name)
case (None, Some(name)) if !NameConstraints.isValidRelationName(name) => throw InvalidRelationName(name)
case (Some(name), _) => name
case (None, Some(name)) => name
case (None, None) => generateRelationName
case (Some(name), _) => name
case (None, Some(name)) => name
case (None, None) => generateRelationName
}

val previousModelAName = schemaMapping.getPreviousModelName(modelA)
val previousModelBName = schemaMapping.getPreviousModelName(modelB)

Expand Down Expand Up @@ -277,7 +277,17 @@ case class SchemaInferrerImpl(
case None => nextRelation
}
}
tmp.groupBy(_.name).values.flatMap(_.headOption).toSet

val grouped: Map[String, Vector[RelationTemplate]] = tmp.groupBy(_.name)

//two unnamed relations whose autogenerated names would be shortened to the same name
grouped.foreach { group =>
val first = group._2.head
val wrongDuplicate = group._2.find(template => !template.connectsTheModels(first.modelAName, first.modelBName))
wrongDuplicate.map(x => throw RelationNameNeeded(group._1, first.modelAName, first.modelBName, x.modelAName, x.modelBName))
}

grouped.values.flatMap(_.headOption).toSet
}

def relationManifestationOnFieldOrRelatedField(
Expand All @@ -286,7 +296,6 @@ case class SchemaInferrerImpl(
relationName: String
): Option[RelationLinkManifestation] = {
if (!isLegacy) { //new
import RelationStrategy._

relationField.relatedField match {
case Some(relatedField) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,14 +259,9 @@ case class DataModelValidatorImpl(
}
}

private def isSelfRelation(fieldAndType: FieldAndType): Boolean = fieldAndType.fieldDef.typeName == fieldAndType.objectType.name
private def isRelationField(fieldAndType: FieldAndType): Boolean = isRelationField(fieldAndType.fieldDef)
private def isRelationField(fieldDef: FieldDefinition): Boolean = !isScalarField(fieldDef) && !isEnumField(fieldDef)

private def isScalarField(fieldAndType: FieldAndType): Boolean = isScalarField(fieldAndType.fieldDef)
private def isScalarField(fieldDef: FieldDefinition): Boolean = fieldDef.hasScalarType

private def isEnumField(fieldDef: FieldDefinition): Boolean = doc.isEnumType(fieldDef.typeName)
private def isRelationField(fieldDef: FieldDefinition): Boolean = !isScalarField(fieldDef) && !isEnumField(fieldDef)
private def isScalarField(fieldDef: FieldDefinition): Boolean = fieldDef.hasScalarType
private def isEnumField(fieldDef: FieldDefinition): Boolean = doc.isEnumType(fieldDef.typeName)
}

case class GlobalValidations(doc: Document) {
Expand Down Expand Up @@ -403,6 +398,13 @@ case class ModelValidator(doc: Document, objectType: ObjectTypeDefinition, capab
None
}

val allowOnlyValidNamesInRelationDirectives = relationFieldsWithRelationDirective.flatMap {
case thisType if thisType.fieldDef.relationName.isDefined && !NameConstraints.isValidRelationName(thisType.fieldDef.relationName.get) =>
Some(DeployErrors.relationDirectiveHasInvalidName(thisType))
case _ =>
None
}

/**
* The validation below must be only applied to fields that specify the relation directive.
* And it can only occur for relation that specify both sides of a relation.
Expand All @@ -428,7 +430,7 @@ case class ModelValidator(doc: Document, objectType: ObjectTypeDefinition, capab
Iterable.empty
}

schemaErrors ++ relationFieldsWithNonMatchingTypes ++ allowOnlyOneDirectiveOnlyWhenUnambiguous
schemaErrors ++ relationFieldsWithNonMatchingTypes ++ allowOnlyOneDirectiveOnlyWhenUnambiguous ++ allowOnlyValidNamesInRelationDirectives
}

def partition[A, B, C](seq: Seq[A])(partitionFn: A => Either[B, C]): (Seq[B], Seq[C]) = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ object DeployErrors {
error(fieldAndType, s"""The field `${fieldAndType.fieldDef.name}` is a scalar field and cannot specify the `@relation` directive.""")
}

def relationDirectiveHasInvalidName(fieldAndType: FieldAndType): DeployError = {
error(
fieldAndType,
s"""The field `${fieldAndType.fieldDef.name}` has an invalid name in the `@relation` directive. It can only have up to 54 characters and must have the shape [A-Z][a-zA-Z0-9]*"""
)
}

def ambiguousRelationSinceThereIsOnlyOneRelationDirective(fieldAndType: FieldAndType): DeployError = {
val relationName = fieldAndType.fieldDef.previousRelationName.get
val nameA = fieldAndType.objectType.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,15 @@ case class LegacyDataModelValidator(
}
}

def ensureRelationDirectivesHaveValidNames(fieldAndType: FieldAndType): Option[DeployError] = {
if (fieldAndType.fieldDef.hasRelationDirectiveWithNameArg && fieldAndType.fieldDef.relationName.isDefined && !NameConstraints.isValidRelationName(
fieldAndType.fieldDef.relationName.get)) {
Some(DeployErrors.relationDirectiveHasInvalidName(fieldAndType))
} else {
None
}
}

def ensureNoOldDefaultValueDirectives(fieldAndTypes: FieldAndType): Option[DeployError] = {
if (fieldAndType.fieldDef.hasOldDefaultValueDirective) {
Some(DeployErrors.invalidSyntaxForDefaultValue(fieldAndType))
Expand Down Expand Up @@ -448,6 +457,7 @@ case class LegacyDataModelValidator(
ensureDirectivesAreUnique(fieldAndType) ++
ensureNoOldDefaultValueDirectives(fieldAndType) ++
ensureRelationDirectivesArePlacedCorrectly(fieldAndType) ++
ensureRelationDirectivesHaveValidNames(fieldAndType) ++
ensureNoDefaultValuesOnListFields(fieldAndType) ++
ensureNoInvalidEnumValuesInDefaultValues(fieldAndType) ++
ensureDefaultValuesHaveCorrectType(fieldAndType)
Expand Down Expand Up @@ -492,9 +502,8 @@ case class LegacyDataModelValidator(
}
}

def isSelfRelation(fieldAndType: FieldAndType): Boolean = fieldAndType.fieldDef.typeName == fieldAndType.objectType.name
def isRelationField(fieldAndType: FieldAndType): Boolean = isRelationField(fieldAndType.fieldDef)
def isRelationField(fieldDef: FieldDefinition): Boolean = !isScalarField(fieldDef) && !isEnumField(fieldDef)
def isSelfRelation(fieldAndType: FieldAndType): Boolean = fieldAndType.fieldDef.typeName == fieldAndType.objectType.name
def isRelationField(fieldDef: FieldDefinition): Boolean = !isScalarField(fieldDef) && !isEnumField(fieldDef)

def isScalarField(fieldAndType: FieldAndType): Boolean = isScalarField(fieldAndType.fieldDef)
def isScalarField(fieldDef: FieldDefinition): Boolean = fieldDef.hasScalarType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ case class InvalidServiceStage(stage: String) extends AbstractDeployApiError(Inv

//case class InvalidDeployment(deployErrorMessage: String) extends AbstractDeployApiError(deployErrorMessage, 4003)

case class InvalidRelationName(relationName: String) extends AbstractDeployApiError(InvalidNames.forRelation(relationName, "relation"), 4004)

case class RelationNameNeeded(relationName: String, firstA: String, firstB: String, secondA: String, secondB: String)
extends AbstractDeployApiError(
"There was an error during the autogeneration of relation names.\n" +
s"Prisma generated the name $relationName twice since it had to be shortened, once for:\n" +
s"A relation between $firstA and $firstB\n" +
s"A relation between $secondA and $secondB\n" +
s"Please name at least one of the relations.",
4004
)
case class ProjectAlreadyExists(name: String, stage: String)
extends AbstractDeployApiError(s"Service with name '$name' and stage '$stage' already exists", 4005)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.prisma.deploy.migration.inference

import com.prisma.deploy.connector.InferredTables
import com.prisma.deploy.migration.validation.LegacyDataModelValidator
import com.prisma.deploy.schema.RelationNameNeeded
import com.prisma.deploy.specutils.DeploySpecBase
import com.prisma.shared.models.ConnectorCapability.{LegacyDataModelCapability, MigrationsCapability}
import com.prisma.shared.models._
Expand All @@ -28,6 +29,50 @@ class LegacyInfererIntegrationSpec extends FlatSpec with Matchers with DeploySpe
steps should be(empty)
}

"they" should "should error if Prisma would shorten two autogenerated relation names to the same name " in {
val schema =
"""
|type Collection {
| id: ID! @unique
| title: String!
| description: String
| liveFrom: DateTime!
| liveTill: DateTime
| autoCurationRules: [CollectionAutoCurationRule!]! @relation(name: "CollectionAutoCurationRules", onDelete: CASCADE)
| applyAllRules: Boolean! @default(value: "false")
| createdAt: DateTime!
| updatedAt: DateTime!
|}
|
|type CollectionAutoCurationRule {
| id: ID! @unique
| collection: Collection! @relation(name: "CollectionAutoCurationRules", onDelete: SET_NULL)
| matchThis: CollectionAutoCurationRuleMatch!
| matchCondition: CollectionAutoCurationRuleCondition!
| matchTo: String!
| createdAt: DateTime!
| updatedAt: DateTime!
|}
|
|type CollectionAutoCurationRuleMatch {
| id: ID! @unique
| match: String! @unique # Tag, Category, etc.
| createdAt: DateTime!
| updatedAt: DateTime!
|}
|
|type CollectionAutoCurationRuleCondition {
| id: ID! @unique
| condition: String! @unique
| createdAt: DateTime!
| updatedAt: DateTime!
|}
|
""".stripMargin

assertThrows[RelationNameNeeded](inferSchema(schema))
}

"they" should "only propose an UpdateRelation step when relation directives get removed" in {
val previousSchema =
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ class LegacyDataModelValidatorSpec extends WordSpecLike with Matchers with Deplo
result.forall(_.description.contains("A relation directive cannot appear more than twice.")) should be(true)
}

"fail if a relationName is invalid" in {
val dataModelString =
"""
|type Todo {
| title: String
| comments: [Comment] @relation(name: "invalidTodoToComments")
|}
|
|type Comment {
| todo: Todo! @relation(name: "invalidTodoToComments")
| text: String
|}
""".stripMargin
val result = validate(dataModelString, isActive = true, capabilities = Set(MigrationsCapability))
result should have(size(2))
result.forall(_.description.contains("It can only have up to 54 characters and must have the shape [A-Z][a-zA-Z0-9]*")) should be(true)
}

// TODO: the backwards field should not be required here.
"succeed if ambiguous relation fields specify the relation directive" in {
val dataModelString =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,21 +328,11 @@ class DeployMutationSpec extends FlatSpec with Matchers with ActiveDeploySpecBas
|}
""".stripMargin

server.queryThatMustFail(
s"""
|mutation {
| deploy(input:{name: "${nameAndStage.name}", stage: "${nameAndStage.stage}", types: ${formatSchema(updatedSchema)}}){
| migration {
| applied
| }
| errors {
| description
| }
| }
|}""".stripMargin,
errorCode = 4004,
errorContains = "ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ is not a valid name for a relation."
)
val res = server.deploySchemaThatMustErrorWithCode(project, updatedSchema, false, 0)

res.toString should be(
"""{"data":{"deploy":{"migration":null,"errors":[{"description":"The field `t2` has an invalid name in the `@relation` directive. It can only have up to 54 characters and must have the shape [A-Z][a-zA-Z0-9]*"}],"warnings":[]}}}""".stripMargin)

}

"DeployMutation" should "shorten autogenerated relationNames to a maximum of 54 characters" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ class Model(
lazy val relationListFields: List[RelationField] = relationFields.filter(_.isList)
lazy val relationNonListFields: List[RelationField] = relationFields.filter(!_.isList)
lazy val visibleRelationFields: List[RelationField] = relationFields.filter(_.isVisible)
lazy val nonListFields = fields.filter(!_.isList)
lazy val idField = scalarFields.find(_.isId)
lazy val createdAtField = scalarFields.find(_.isCreatedAt)
lazy val updatedAtField = scalarFields.find(_.isUpdatedAt)
lazy val idField_! = idField.getOrElse(sys.error(s"The model $name has no id field!"))
lazy val dbNameOfIdField_! = idField_!.dbName
lazy val hasUpdatedAtField = scalarFields.exists(_.isUpdatedAt)
lazy val hasCreatedAtField = scalarFields.exists(_.isCreatedAt)
lazy val nonListFields: List[Field] = fields.filter(!_.isList)
lazy val idField: Option[ScalarField] = scalarFields.find(_.isId)
lazy val createdAtField: Option[ScalarField] = scalarFields.find(_.isCreatedAt)
lazy val updatedAtField: Option[ScalarField] = scalarFields.find(_.isUpdatedAt)
lazy val idField_! : ScalarField = idField.getOrElse(sys.error(s"The model $name has no id field!"))
lazy val dbNameOfIdField_! : String = idField_!.dbName
lazy val hasUpdatedAtField: Boolean = scalarFields.exists(_.isUpdatedAt)
lazy val hasCreatedAtField: Boolean = scalarFields.exists(_.isCreatedAt)
lazy val hasVisibleIdField: Boolean = idField.exists(_.isVisible)
def dummyField(rf: RelationField): ScalarField =
idField_!.copy(name = rf.name,
Expand Down Expand Up @@ -77,5 +77,4 @@ class Model(
def getFieldByName(name: String): Option[Field] = fields.find(_.name == name)
def getFieldByDBName_!(name: String): Field = getFieldByDBName(name).getOrElse(sys.error(s"field $name is not part of the model ${this.name}"))
def getFieldByDBName(name: String): Option[Field] = fields.find(_.dbName == name)

}

0 comments on commit 496fb21

Please sign in to comment.