diff --git a/.changeset/spotty-kangaroos-refuse.md b/.changeset/spotty-kangaroos-refuse.md new file mode 100644 index 0000000..f9a023d --- /dev/null +++ b/.changeset/spotty-kangaroos-refuse.md @@ -0,0 +1,5 @@ +--- +"amplience-graphql-codegen-json": minor +--- + +- add validation for amplience extension diff --git a/packages/plugin-json/src/index.ts b/packages/plugin-json/src/index.ts index 62673c3..ce34b7e 100644 --- a/packages/plugin-json/src/index.ts +++ b/packages/plugin-json/src/index.ts @@ -13,6 +13,8 @@ import type { ObjectTypeDefinitionNode } from "graphql"; import { dirname } from "path"; import { contentTypeSchemaBody } from "./lib/amplience-schema-transformers"; import { + getAmplienceExtensionNotNullableObjectReport, + getAmplienceExtensionReferencesAmplienceContentTypeReport, getDeliveryKeyNotNullableStringReport, getObjectTypeDefinitions, getRequiredLocalizedFieldsReport, @@ -86,6 +88,22 @@ export const validate: PluginValidateFn = ( `Fields with '@amplienceDeliveryKey' must be of Nullable type String.\n\n${deliveryKeyNotNullableStringReport}`, ); } + + const amplienceExtensionNotNullableObjectReport = + getAmplienceExtensionNotNullableObjectReport(types); + if (amplienceExtensionNotNullableObjectReport) { + throw new Error( + `Fields with '@amplienceExtension' must be Nullable and of an Object type defined elsewhere in the schema.\n\n${amplienceExtensionNotNullableObjectReport}`, + ); + } + + const amplienceExtensionOnAmplienceContentTypeReport = + getAmplienceExtensionReferencesAmplienceContentTypeReport(types); + if (amplienceExtensionOnAmplienceContentTypeReport) { + throw new Error( + `Types referenced by fields with '@amplienceExtension' must not have '@amplienceContentType' directive.\n\n${amplienceExtensionOnAmplienceContentTypeReport}`, + ); + } }; export const preset: Types.OutputPreset = { diff --git a/packages/plugin-json/src/lib/validation.ts b/packages/plugin-json/src/lib/validation.ts index 79f682b..2b6b8bc 100644 --- a/packages/plugin-json/src/lib/validation.ts +++ b/packages/plugin-json/src/lib/validation.ts @@ -3,10 +3,11 @@ import { isObjectTypeDefinitionNode, isValue, } from "amplience-graphql-codegen-common"; -import type { - FieldDefinitionNode, - GraphQLSchema, - ObjectTypeDefinitionNode, +import type { NamedTypeNode, TypeNode } from "graphql"; +import { + type FieldDefinitionNode, + type GraphQLSchema, + type ObjectTypeDefinitionNode, } from "graphql"; export const getObjectTypeDefinitions = ( @@ -82,6 +83,66 @@ const isDeliveryKeyField = (field: FieldDefinitionNode) => const isNullableStringField = (field: FieldDefinitionNode) => field.type.kind === "NamedType" && field.type.name.value === "String"; +export const getAmplienceExtensionNotNullableObjectReport = ( + types: ObjectTypeDefinitionNode[], +): string => + getFieldsReport( + types.filter( + (type) => + type.fields?.some( + (field) => + isAmplienceExtensionField(field) && + !isNullableObjectField(field.type, types), + ), + ), + (field) => + isAmplienceExtensionField(field) && + !isNullableObjectField(field.type, types), + ); + +// @amplienceExtension referencing an @amplienceContentType produces odd behavior we want to avoid +export const getAmplienceExtensionReferencesAmplienceContentTypeReport = ( + types: ObjectTypeDefinitionNode[], +): string => + getFieldsReport( + types.filter( + (type) => + type.fields?.some( + (field) => + isAmplienceExtensionField(field) && + isNullableObjectField(field.type, types) && + isAmplienceContentTypeField(field.type, types), + ), + ), + (field) => + isAmplienceExtensionField(field) && + isNullableObjectField(field.type, types) && + isAmplienceContentTypeField(field.type, types), + ); + +const isAmplienceExtensionField = (field: FieldDefinitionNode) => + hasDirective(field, "amplienceExtension"); + +const isNullableObjectField = ( + fieldType: TypeNode, + allTypes: ObjectTypeDefinitionNode[], +): fieldType is NamedTypeNode => + fieldType.kind === "NamedType" && + allTypes.some((type) => type.name.value === fieldType.name.value); + +const isAmplienceContentTypeField = ( + fieldType: NamedTypeNode, + allTypes: ObjectTypeDefinitionNode[], +) => { + const referencedType = allTypes.find( + (t) => t.name.value === fieldType.name.value, + ); + + return referencedType + ? hasDirective(referencedType, "amplienceContentType") + : false; +}; + /** * Converts a type with filtered fields in a simple string report. * diff --git a/packages/plugin-json/test/validate.test.ts b/packages/plugin-json/test/validate.test.ts index 9324682..ea5a457 100644 --- a/packages/plugin-json/test/validate.test.ts +++ b/packages/plugin-json/test/validate.test.ts @@ -121,3 +121,102 @@ it.each([ ); }, ); + +it.each([ + gql` + type Test { + a: String! @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Int! @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Float! @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Boolean! @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: String @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Int @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Float @amplienceExtension(name: "test-extension") + } + `, + gql` + type Test { + a: Boolean @amplienceExtension(name: "test-extension") + } + `, + gql` + type ObjectType { + a: String! + } + type Test { + a: [ObjectType] @amplienceExtension(name: "test-extension") + } + `, + gql` + type ObjectType { + a: String! + } + type Test { + a: ObjectType! @amplienceExtension(name: "test-extension") + } + `, + gql` + type ObjectType { + a: String! + } + type Test { + a: [ObjectType!]! @amplienceExtension(name: "test-extension") + } + `, + gql` + scalar Date + type Test { + a: Date @amplienceExtension(name: "test-extension") + } + `, +])( + "Throw error: Fields with '@amplienceExtension' must be Nullable and of an Object type defined elsewhere in the schema", + (testSchema) => { + const schema = buildASTSchema(gql` + ${print(schemaPrepend)} + ${print(testSchema)} + `); + expect(() => validate(schema, [], {}, "", [])).toThrow( + "Fields with '@amplienceExtension' must be Nullable and of an Object type defined elsewhere in the schema.\n\ntype Test\n\ta", + ); + }, +); + +it("Throws error: Types referenced by fields with '@amplienceExtension' must not have '@amplienceContentType' directive", () => { + const schema = buildASTSchema(gql` + ${print(schemaPrepend)} + type ReferencedType @amplienceContentType { + a: String! + } + type Test { + a: ReferencedType @amplienceExtension(name: "test-extension") + } + `); + expect(() => validate(schema, [], {}, "", [])).toThrow( + "Types referenced by fields with '@amplienceExtension' must not have '@amplienceContentType' directive.\n\ntype Test\n\ta", + ); +});