Skip to content

Commit

Permalink
feat: validation for @amplienceExtension (#98)
Browse files Browse the repository at this point in the history
* feat: validation for @amplienceExtension

* refactor: improved object type validation

* chore: add changeset
  • Loading branch information
jonnoallcock authored Jun 19, 2024
1 parent b7858f1 commit dd92fc4
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/spotty-kangaroos-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"amplience-graphql-codegen-json": minor
---

- add validation for amplience extension
18 changes: 18 additions & 0 deletions packages/plugin-json/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,6 +88,22 @@ export const validate: PluginValidateFn<PluginConfig> = (
`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<PresetConfig> = {
Expand Down
69 changes: 65 additions & 4 deletions packages/plugin-json/src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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.
*
Expand Down
99 changes: 99 additions & 0 deletions packages/plugin-json/test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});

0 comments on commit dd92fc4

Please sign in to comment.