From db483991d6b59e64e016c1864c05e61b167791d9 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:09:03 +0100 Subject: [PATCH 01/10] add directives to supermassive AST --- .../supermassive/src/schema/definition.ts | 112 +++++- .../encodeASTSchema.test.ts.snap | 354 ++++++++++++++++++ .../__tests__/encodeASTSchema.test.ts | 7 + .../__tests__/fixtures/kitchenSinkSDL.ts | 14 + .../__tests__/mergeSchemaDefinitions.test.ts | 2 +- .../src/utilities/decodeASTSchema.ts | 107 +++++- .../src/utilities/encodeASTSchema.ts | 136 ++++++- .../src/utilities/mergeSchemaDefinitions.ts | 32 ++ 8 files changed, 724 insertions(+), 40 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 9de6f5796..520cfc098 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -18,56 +18,65 @@ const enum TypeKind { export type ScalarTypeDefinitionTuple = [ kind: TypeKind.SCALAR, - // directives?: DirectiveTuple[], // TODO ? + directives?: DirectiveTuple[], ]; +const enum ScalarKeys { + directives = 1, +} + export type ObjectTypeDefinitionTuple = [ kind: TypeKind.OBJECT, fields: FieldDefinitionRecord, interfaces?: TypeName[], - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum ObjectKeys { fields = 1, interfaces = 2, + directives = 3, } export type InterfaceTypeDefinitionTuple = [ kind: TypeKind.INTERFACE, fields: FieldDefinitionRecord, interfaces?: TypeName[], - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum InterfaceKeys { fields = 1, interfaces = 2, + directives = 3, } export type UnionTypeDefinitionTuple = [ kind: TypeKind.UNION, types: TypeName[], - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum UnionKeys { types = 1, + directives = 2, } export type EnumTypeDefinitionTuple = [ kind: TypeKind.ENUM, values: string[], - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum EnumKeys { values = 1, + directives = 2, } export type InputObjectTypeDefinitionTuple = [ kind: TypeKind.INPUT, fields: InputValueDefinitionRecord, - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum InputObjectKeys { fields = 1, + directives = 2, } export type TypeDefinitionTuple = @@ -86,7 +95,7 @@ export type CompositeTypeTuple = export type FieldDefinitionTuple = [ type: TypeReference, arguments: InputValueDefinitionRecord, - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum FieldKeys { type = 0, @@ -98,7 +107,7 @@ export type FieldDefinitionRecord = Record; export type InputValueDefinitionTuple = [ type: TypeReference, defaultValue: unknown, - // directives?: DirectiveTuple[], + directives?: DirectiveTuple[], ]; const enum InputValueKeys { type = 0, @@ -157,7 +166,7 @@ const DirectiveLocationToGraphQL = { export type DirectiveName = string; export type DirectiveTuple = [ name: DirectiveName, - arguments: Record, // JS values (cannot be a variable inside schema definition, so it is fine) + arguments?: Record, // JS values (cannot be a variable inside schema definition, so it is fine) ]; export type DirectiveDefinitionTuple = [ name: DirectiveName, @@ -530,6 +539,12 @@ export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { return tuple[EnumKeys.values]; } +export function getEnumDirectives( + tuple: EnumTypeDefinitionTuple, +): DirectiveTuple[] { + return tuple[EnumKeys.directives] || []; +} + export function getDirectiveDefinitionArgs( directive: DirectiveDefinitionTuple, ): InputValueDefinitionRecord | undefined { @@ -546,44 +561,91 @@ export function setDirectiveDefinitionArgs( export function createUnionTypeDefinition( types: TypeName[], + directives?: DirectiveTuple[], ): UnionTypeDefinitionTuple { + if (directives?.length) { + return [TypeKind.UNION, types, directives]; + } + return [TypeKind.UNION, types]; } export function createInterfaceTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], + directives?: DirectiveTuple[], ): InterfaceTypeDefinitionTuple { - return interfaces?.length - ? [TypeKind.INTERFACE, fields, interfaces] - : [TypeKind.INTERFACE, fields]; + if (!interfaces?.length && !directives?.length) { + return [TypeKind.INTERFACE, fields]; + } + + if (interfaces?.length && !directives?.length) { + return [TypeKind.INTERFACE, fields, interfaces]; + } + + return [TypeKind.INTERFACE, fields, interfaces, directives]; } export function createObjectTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], + directives?: DirectiveTuple[], ): ObjectTypeDefinitionTuple { - return interfaces?.length - ? [TypeKind.OBJECT, fields, interfaces] - : [TypeKind.OBJECT, fields]; + if (!interfaces?.length && !directives?.length) { + return [TypeKind.OBJECT, fields]; + } + + if (interfaces?.length && !directives?.length) { + return [TypeKind.OBJECT, fields, interfaces]; + } + + return [TypeKind.OBJECT, fields, interfaces, directives]; } export function createInputObjectTypeDefinition( fields: InputValueDefinitionRecord, + directives?: DirectiveTuple[], ): InputObjectTypeDefinitionTuple { + if (directives?.length) { + return [TypeKind.INPUT, fields, directives]; + } + return [TypeKind.INPUT, fields]; } export function createEnumTypeDefinition( values: string[], + directives?: DirectiveTuple[], ): EnumTypeDefinitionTuple { + if (directives?.length) { + return [TypeKind.ENUM, values, directives]; + } + return [TypeKind.ENUM, values]; } -export function createScalarTypeDefinition(): ScalarTypeDefinitionTuple { +export function createScalarTypeDefinition( + directives?: DirectiveTuple[], +): ScalarTypeDefinitionTuple { + if (directives?.length) { + return [TypeKind.SCALAR, directives]; + } + return [TypeKind.SCALAR]; } +export function getScalarTypeDirectives( + def: ScalarTypeDefinitionTuple, +): DirectiveTuple[] { + return def[ScalarKeys.directives] ?? []; +} + +export function getObjectTypeDirectives( + def: ObjectTypeDefinitionTuple, +): DirectiveTuple[] { + return def[ObjectKeys.directives] ?? []; +} + export function getObjectTypeInterfaces( def: ObjectTypeDefinitionTuple, ): TypeName[] { @@ -596,12 +658,30 @@ export function getInterfaceTypeInterfaces( return def[InterfaceKeys.interfaces] ?? []; } +export function getInterfaceTypeDiretives( + def: InterfaceTypeDefinitionTuple, +): DirectiveTuple[] { + return def[InterfaceKeys.directives] ?? []; +} + +export function getInputTypeDirectives( + def: InputObjectTypeDefinitionTuple, +): DirectiveTuple[] { + return def[InputObjectKeys.directives] ?? []; +} + export function getUnionTypeMembers( tuple: UnionTypeDefinitionTuple, ): TypeName[] { return tuple[UnionKeys.types]; } +export function getUnionTypeDirectives( + def: UnionTypeDefinitionTuple, +): DirectiveTuple[] { + return def[UnionKeys.directives] ?? []; +} + export function getFieldArguments( def: FieldDefinition, ): InputValueDefinitionRecord | undefined { diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index ca0fc0c96..f6bfbf2de 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -354,6 +354,360 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` ] `; +exports[`encodeASTSchema correctly encodes kitchen sink AST schema with directives 1`] = ` +[ + { + "directives": [ + [ + "skip", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "include", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "include2", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "myRepeatableDir", + [ + 11, + 14, + ], + { + "name": 6, + }, + ], + ], + "types": { + "AnnotatedEnum": [ + 5, + [ + "ANNOTATED_VALUE", + "OTHER_VALUE", + ], + ], + "AnnotatedInput": [ + 6, + { + "annotatedField": "Type", + }, + ], + "AnnotatedInterface": [ + 3, + { + "annotatedField": [ + "Type", + { + "arg": "Type", + }, + ], + }, + ], + "AnnotatedObject": [ + 2, + { + "annotatedField": [ + "Type", + { + "arg": [ + "Type", + "default", + ], + }, + ], + }, + ], + "AnnotatedScalar": [ + 1, + ], + "AnnotatedUnion": [ + 4, + [ + "A", + "B", + ], + ], + "AnnotatedUnionTwo": [ + 4, + [ + "A", + "B", + ], + ], + "Bar": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + }, + ], + "Baz": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Bar", + "Two", + ], + ], + "CustomScalar": [ + 1, + ], + "Feed": [ + 4, + [ + "Story", + "Article", + "Advert", + ], + ], + "Foo": [ + 2, + { + "eight": [ + "Type", + { + "argument": "OneOfInputType", + }, + ], + "five": [ + 1, + { + "argument": [ + 11, + [ + "string", + "string", + ], + ], + }, + ], + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + "seven": [ + "Type", + { + "argument": [ + 3, + null, + ], + }, + ], + "six": [ + "Type", + { + "argument": [ + "InputType", + { + "key": "value", + }, + ], + }, + ], + "three": [ + 3, + { + "argument": "InputType", + "other": 1, + }, + ], + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Bar", + "Baz", + "Two", + ], + ], + "InputType": [ + 6, + { + "answer": [ + 3, + 42, + ], + "key": 6, + }, + ], + "OneOfInputType": [ + 6, + { + "int": 3, + "string": 1, + }, + ], + "Site": [ + 5, + [ + "DESKTOP", + "MOBILE", + "WEB", + ], + ], + "UndefinedEnum": [ + 5, + [], + ], + "UndefinedInput": [ + 6, + {}, + ], + "UndefinedInterface": [ + 3, + {}, + ], + "UndefinedType": [ + 2, + {}, + ], + "UndefinedUnion": [ + 4, + [], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + { + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Two", + ], + ], + "CustomScalar": [ + 1, + ], + "Feed": [ + 4, + [ + "Photo", + "Video", + ], + ], + "Foo": [ + 2, + { + "seven": [ + "Type", + { + "argument": 11, + }, + ], + }, + ], + "InputType": [ + 6, + { + "other": [ + 4, + 12300, + ], + }, + ], + "Site": [ + 5, + [ + "VR", + ], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + {}, + ], + "Feed": [ + 4, + [], + ], + "Foo": [ + 2, + {}, + ], + "InputType": [ + 6, + {}, + ], + "Site": [ + 5, + [], + ], + }, + }, +] +`; + exports[`encodeASTSchema correctly encodes swapi AST schema 1`] = ` [ { diff --git a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts index d4e31b3d1..f3b61b2e8 100644 --- a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts @@ -12,4 +12,11 @@ describe(encodeASTSchema, () => { const encoded = encodeASTSchema(kitchenSinkSDL.document); expect(encoded).toMatchSnapshot(); }); + + test("correctly encodes kitchen sink AST schema with directives", () => { + const encoded = encodeASTSchema(kitchenSinkSDL.document, { + includeDirectives: true, + }); + expect(encoded).toMatchSnapshot(); + }); }); diff --git a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts index 6ee03283d..e16751f4a 100644 --- a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts +++ b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts @@ -155,6 +155,20 @@ export const kitchenSinkSDL = gql` directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE + directive @onType on OBJECT + + directive @onObject(arg: String!) on OBJECT + + directive @onUnion on UNION + + directive @onInterface on INTERFACE + + directive @onScalar on SCALAR + + directive @onEnum on ENUM + + directive @onInputObject on INPUT_OBJECT + extend schema @onSchema extend schema @onSchema { diff --git a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts index 8f793ba00..c9a231213 100644 --- a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts +++ b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts @@ -245,7 +245,7 @@ describe("mergeSchemaDefinitions", () => { } extend type User implements Named { - name: String + name: String @testDirective } extend type User implements Contactable { diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index f18bc3029..8e706244d 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -47,6 +47,15 @@ import { getDirectiveDefinitionArgs, getDirectiveLocations, decodeDirectiveLocation, + getObjectTypeDirectives, + getInterfaceTypeDiretives, + getEnumDirectives, + getUnionTypeDirectives, + ScalarTypeDefinitionTuple, + getScalarTypeDirectives, + getInputTypeDirectives, + getDirectiveArguments, + DirectiveTuple, } from "../schema/definition"; import { inspectTypeReference, @@ -57,7 +66,10 @@ import { unwrap, } from "../schema/reference"; import { invariant } from "../jsutils/invariant"; -import { ValueNode as ConstValueNode } from "graphql/language/ast"; // TODO: use ConstValueNode in graphql@17 +import { + ValueNode as ConstValueNode, + DirectiveNode, +} from "graphql/language/ast"; // TODO: use ConstValueNode in graphql@17 import { inspect } from "../jsutils/inspect"; /** @@ -77,17 +89,19 @@ export function decodeASTSchema( for (const typeName in types) { const tuple = types[typeName]; if (isScalarTypeDefinition(tuple)) { - definitions.push(decodeScalarType(typeName)); + definitions.push(decodeScalarType(typeName, tuple, types, directives)); } else if (isEnumTypeDefinition(tuple)) { - definitions.push(decodeEnumType(typeName, tuple)); + definitions.push(decodeEnumType(typeName, tuple, types, directives)); } else if (isObjectTypeDefinition(tuple)) { - definitions.push(decodeObjectType(typeName, tuple, types)); + definitions.push(decodeObjectType(typeName, tuple, types, directives)); } else if (isInterfaceTypeDefinition(tuple)) { - definitions.push(decodeInterfaceType(typeName, tuple, types)); + definitions.push(decodeInterfaceType(typeName, tuple, types, directives)); } else if (isUnionTypeDefinition(tuple)) { - definitions.push(decodeUnionType(typeName, tuple)); + definitions.push(decodeUnionType(typeName, tuple, types, directives)); } else if (isInputObjectTypeDefinition(tuple)) { - definitions.push(decodeInputObjectType(typeName, tuple, types)); + definitions.push( + decodeInputObjectType(typeName, tuple, types, directives), + ); } } @@ -102,16 +116,26 @@ function nameNode(value: string): NameNode { return { kind: Kind.NAME, value }; } -function decodeScalarType(typeName: string): ScalarTypeDefinitionNode { +function decodeScalarType( + typeName: string, + tuple: ScalarTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], +): ScalarTypeDefinitionNode { return { kind: Kind.SCALAR_TYPE_DEFINITION, name: nameNode(typeName), + directives: + directives && + decodeDirectiveTuple(getScalarTypeDirectives(tuple), types, directives), }; } function decodeEnumType( typeName: string, tuple: EnumTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { return { kind: Kind.ENUM_TYPE_DEFINITION, @@ -120,6 +144,9 @@ function decodeEnumType( kind: Kind.ENUM_VALUE_DEFINITION, name: nameNode(value), })), + directives: + directives && + decodeDirectiveTuple(getEnumDirectives(tuple), types, directives), }; } @@ -127,6 +154,7 @@ function decodeObjectType( typeName: string, tuple: ObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { return { kind: Kind.OBJECT_TYPE_DEFINITION, @@ -136,6 +164,9 @@ function decodeObjectType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), + directives: + directives && + decodeDirectiveTuple(getObjectTypeDirectives(tuple), types, directives), }; } @@ -143,6 +174,7 @@ function decodeInterfaceType( typeName: string, tuple: InterfaceTypeDefinitionTuple, types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { return { kind: Kind.INTERFACE_TYPE_DEFINITION, @@ -152,12 +184,17 @@ function decodeInterfaceType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), + directives: + directives && + decodeDirectiveTuple(getInterfaceTypeDiretives(tuple), types, directives), }; } function decodeUnionType( typeName: string, tuple: UnionTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { return { kind: Kind.UNION_TYPE_DEFINITION, @@ -166,6 +203,9 @@ function decodeUnionType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), + directives: + directives && + decodeDirectiveTuple(getUnionTypeDirectives(tuple), types, directives), }; } @@ -173,6 +213,7 @@ function decodeInputObjectType( typeName: string, tuple: InputObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, @@ -180,6 +221,9 @@ function decodeInputObjectType( fields: Object.entries(getInputObjectFields(tuple)).map(([name, value]) => decodeInputValue(name, value, types), ), + directives: + directives && + decodeDirectiveTuple(getInputTypeDirectives(tuple), types, directives), }; } @@ -338,3 +382,50 @@ function decodeDirective( repeatable: false, }; } + +function decodeDirectiveTuple( + directiveTuples: DirectiveTuple[], + types: TypeDefinitionsRecord, + directives: DirectiveDefinitionTuple[], +): ReadonlyArray | undefined { + return directiveTuples.map(([directiveName, args]) => { + const directiveTuple = directives?.find( + (directive) => getDirectiveName(directive) === directiveName, + ); + + invariant( + directiveTuple !== undefined, + `Could not find directive definition for "${directiveName}"`, + ); + + const argumentDefinitions = getDirectiveArguments(directiveTuple); + + return { + kind: Kind.DIRECTIVE, + name: nameNode(directiveName), + arguments: + args && argumentDefinitions + ? Object.entries(args)?.map(([argName, argValue]) => { + invariant( + argumentDefinitions[argName] !== undefined, + `Could not find directive argument definition "${argName}"for "${directiveName}" directive`, + ); + + const inputValueTypeRef = getInputValueTypeReference( + argumentDefinitions[argName], + ); + + return { + kind: Kind.ARGUMENT, + name: nameNode(argName), + value: valueToConstValueNode( + argValue, + inputValueTypeRef, + types, + ), + }; + }) + : undefined, + }; + }); +} diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index a8b1511a6..6dc052769 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -16,6 +16,7 @@ import { EnumTypeExtensionNode, ScalarTypeExtensionNode, DirectiveLocationEnum, + DirectiveNode, } from "graphql"; import { DirectiveDefinitionTuple, @@ -37,42 +38,88 @@ import { createInterfaceTypeDefinition, createUnionTypeDefinition, encodeDirectiveLocation, + DirectiveTuple, } from "../schema/definition"; import { typeReferenceFromNode, TypeReference } from "../schema/reference"; import { valueFromASTUntyped } from "./valueFromASTUntyped"; +type EncodeASTSchemaOptions = { + includeDirectives?: boolean; +}; + export function encodeASTSchema( schemaFragment: DocumentNode, + options?: EncodeASTSchemaOptions, ): SchemaDefinitions[] { + const includeDirectives = Boolean(options?.includeDirectives); const fragments: SchemaDefinitions[] = [{ types: {} }]; const add = (name: string, def: TypeDefinitionTuple, extension = false) => addTypeDefinition(fragments, name, def, extension); for (const definition of schemaFragment.definitions) { if (definition.kind === "ObjectTypeDefinition") { - add(definition.name.value, encodeObjectType(definition)); + add( + definition.name.value, + encodeObjectType(definition, includeDirectives), + ); } else if (definition.kind === "InputObjectTypeDefinition") { - add(definition.name.value, encodeInputObjectType(definition)); + add( + definition.name.value, + encodeInputObjectType(definition, includeDirectives), + ); } else if (definition.kind === "EnumTypeDefinition") { - add(definition.name.value, encodeEnumType(definition)); + add(definition.name.value, encodeEnumType(definition, includeDirectives)); } else if (definition.kind === "UnionTypeDefinition") { - add(definition.name.value, encodeUnionType(definition)); + add( + definition.name.value, + encodeUnionType(definition, includeDirectives), + ); } else if (definition.kind === "InterfaceTypeDefinition") { - add(definition.name.value, encodeInterfaceType(definition)); + add( + definition.name.value, + encodeInterfaceType(definition, includeDirectives), + ); } else if (definition.kind === "ScalarTypeDefinition") { - add(definition.name.value, encodeScalarType(definition)); + add( + definition.name.value, + encodeScalarType(definition, includeDirectives), + ); } else if (definition.kind === "ObjectTypeExtension") { - add(definition.name.value, encodeObjectType(definition), true); + add( + definition.name.value, + encodeObjectType(definition, includeDirectives), + true, + ); } else if (definition.kind === "InputObjectTypeExtension") { - add(definition.name.value, encodeInputObjectType(definition), true); + add( + definition.name.value, + encodeInputObjectType(definition, includeDirectives), + true, + ); } else if (definition.kind === "EnumTypeExtension") { - add(definition.name.value, encodeEnumType(definition), true); + add( + definition.name.value, + encodeEnumType(definition, includeDirectives), + true, + ); } else if (definition.kind === "UnionTypeExtension") { - add(definition.name.value, encodeUnionType(definition), true); + add( + definition.name.value, + encodeUnionType(definition, includeDirectives), + true, + ); } else if (definition.kind === "InterfaceTypeExtension") { - add(definition.name.value, encodeInterfaceType(definition), true); + add( + definition.name.value, + encodeInterfaceType(definition, includeDirectives), + true, + ); } else if (definition.kind === "ScalarTypeExtension") { - add(definition.name.value, encodeScalarType(definition), true); + add( + definition.name.value, + encodeScalarType(definition, includeDirectives), + true, + ); } else if (definition.kind === "DirectiveDefinition") { if (!fragments[0].directives) { fragments[0].directives = []; @@ -106,21 +153,35 @@ function addTypeDefinition( } function encodeScalarType( - _type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, + type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, + includeDirectives: boolean, ): ScalarTypeDefinitionTuple { - return createScalarTypeDefinition(); + return createScalarTypeDefinition( + includeDirectives + ? type.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, + ); } function encodeEnumType( node: EnumTypeDefinitionNode | EnumTypeExtensionNode, + includeDirectives: boolean, ): EnumTypeDefinitionTuple { return createEnumTypeDefinition( (node.values ?? []).map((value) => value.name.value), + includeDirectives + ? node.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, ); } function encodeObjectType( node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, + includeDirectives: boolean, ): ObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { @@ -129,11 +190,37 @@ function encodeObjectType( return createObjectTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), + includeDirectives + ? node.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, ); } +function encodeDirectiveTuple( + directive?: DirectiveNode, +): DirectiveTuple | undefined { + if (!directive) { + return; + } + + const name = directive.name.value; + + const args = Object.create(null); + for (const argument of directive.arguments ?? []) { + args[argument.name.value] = valueFromASTUntyped(argument.value); + } + + if (Object.keys(args).length) { + return [name, args]; + } + return [name]; +} + function encodeInterfaceType( node: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, + includeDirectives: boolean, ): InterfaceTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { @@ -142,25 +229,44 @@ function encodeInterfaceType( return createInterfaceTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), + includeDirectives + ? node.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, ); } function encodeUnionType( node: UnionTypeDefinitionNode | UnionTypeExtensionNode, + includeDirectives: boolean, ): UnionTypeDefinitionTuple { return createUnionTypeDefinition( (node.types ?? []).map((type) => type.name.value), + includeDirectives + ? node.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, ); } function encodeInputObjectType( node: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, + includeDirectives: boolean, ): InputObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { fields[field.name.value] = encodeInputValue(field); } - return createInputObjectTypeDefinition(fields); + return createInputObjectTypeDefinition( + fields, + includeDirectives + ? node.directives + ?.map(encodeDirectiveTuple) + .filter((directive) => !!directive) + : undefined, + ); } function encodeField( diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index f7ab9040d..ea1f95943 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -1,5 +1,6 @@ import { DirectiveDefinitionTuple, + DirectiveTuple, FieldDefinitionRecord, getDirectiveDefinitionArgs, getDirectiveName, @@ -16,6 +17,7 @@ import { setDirectiveDefinitionArgs, setFieldArgs, TypeDefinitionsRecord, + TypeDefinitionTuple, } from "../schema/definition"; import { inspect } from "../jsutils/inspect"; @@ -45,6 +47,33 @@ export function mergeSchemaDefinitions( return accumulator; } +function mergeFieldDirectives( + target: TypeDefinitionTuple, + source: TypeDefinitionTuple, +): void { + const targetDirectives: DirectiveTuple[] | undefined = target[3]; + const sourceDirectives: DirectiveTuple[] | undefined = source[3]; + + if (!sourceDirectives) { + return; + } + + if (!targetDirectives) { + target[3] = [...sourceDirectives]; + return; + } + + for (const sourceDirective of sourceDirectives) { + const directiveName = sourceDirective[0]; + const exists = targetDirectives.some( + (d: DirectiveTuple) => d[0] === directiveName, + ); + if (!exists) { + targetDirectives.push(sourceDirective); + } + } +} + export function mergeDirectives( target: DirectiveDefinitionTuple[], source: DirectiveDefinitionTuple[], @@ -84,6 +113,9 @@ export function mergeTypes( target[typeName] = sourceDef; continue; } + + mergeFieldDirectives(targetDef, sourceDef); + if ( (isObjectTypeDefinition(targetDef) && isObjectTypeDefinition(sourceDef)) || From 3a2f5d3c882970e61882054bf755bb37aa911fe3 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:52:45 +0100 Subject: [PATCH 02/10] add directive definition into snapshots --- .../supermassive/src/schema/definition.ts | 24 ++--- .../decodeASTSchema.test.ts.snap | 14 +++ .../encodeASTSchema.test.ts.snap | 90 +++++++++++++++++++ .../__tests__/encodeASTSchema.test.ts | 4 +- .../src/utilities/decodeASTSchema.ts | 6 +- 5 files changed, 122 insertions(+), 16 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 520cfc098..86cd7d4ca 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -541,8 +541,8 @@ export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { export function getEnumDirectives( tuple: EnumTypeDefinitionTuple, -): DirectiveTuple[] { - return tuple[EnumKeys.directives] || []; +): DirectiveTuple[] | undefined { + return tuple[EnumKeys.directives]; } export function getDirectiveDefinitionArgs( @@ -636,14 +636,14 @@ export function createScalarTypeDefinition( export function getScalarTypeDirectives( def: ScalarTypeDefinitionTuple, -): DirectiveTuple[] { - return def[ScalarKeys.directives] ?? []; +): DirectiveTuple[] | undefined { + return def[ScalarKeys.directives]; } export function getObjectTypeDirectives( def: ObjectTypeDefinitionTuple, -): DirectiveTuple[] { - return def[ObjectKeys.directives] ?? []; +): DirectiveTuple[] | undefined { + return def[ObjectKeys.directives]; } export function getObjectTypeInterfaces( @@ -660,14 +660,14 @@ export function getInterfaceTypeInterfaces( export function getInterfaceTypeDiretives( def: InterfaceTypeDefinitionTuple, -): DirectiveTuple[] { - return def[InterfaceKeys.directives] ?? []; +): DirectiveTuple[] | undefined { + return def[InterfaceKeys.directives]; } export function getInputTypeDirectives( def: InputObjectTypeDefinitionTuple, -): DirectiveTuple[] { - return def[InputObjectKeys.directives] ?? []; +): DirectiveTuple[] | undefined { + return def[InputObjectKeys.directives]; } export function getUnionTypeMembers( @@ -678,8 +678,8 @@ export function getUnionTypeMembers( export function getUnionTypeDirectives( def: UnionTypeDefinitionTuple, -): DirectiveTuple[] { - return def[UnionKeys.directives] ?? []; +): DirectiveTuple[] | undefined { + return def[UnionKeys.directives]; } export function getFieldArguments( diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index d69d6c6fd..046ed2daa 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap @@ -97,6 +97,20 @@ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @myRepeatableDir(name: String!) on OBJECT | INTERFACE + +directive @onType on OBJECT + +directive @onObject(arg: String!) on OBJECT + +directive @onUnion on UNION + +directive @onInterface on INTERFACE + +directive @onScalar on SCALAR + +directive @onEnum on ENUM + +directive @onInputObject on INPUT_OBJECT " `; diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index f6bfbf2de..ca1839cba 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -47,6 +47,51 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "name": 6, }, ], + [ + "onType", + [ + 11, + ], + ], + [ + "onObject", + [ + 11, + ], + { + "arg": 6, + }, + ], + [ + "onUnion", + [ + 15, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "onInputObject", + [ + 18, + ], + ], ], "types": { "AnnotatedEnum": [ @@ -401,6 +446,51 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema with directiv "name": 6, }, ], + [ + "onType", + [ + 11, + ], + ], + [ + "onObject", + [ + 11, + ], + { + "arg": 6, + }, + ], + [ + "onUnion", + [ + 15, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "onInputObject", + [ + 18, + ], + ], ], "types": { "AnnotatedEnum": [ diff --git a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts index f3b61b2e8..58b2c1b22 100644 --- a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts @@ -14,9 +14,7 @@ describe(encodeASTSchema, () => { }); test("correctly encodes kitchen sink AST schema with directives", () => { - const encoded = encodeASTSchema(kitchenSinkSDL.document, { - includeDirectives: true, - }); + const encoded = encodeASTSchema(kitchenSinkSDL.document); expect(encoded).toMatchSnapshot(); }); }); diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index 8e706244d..6d812bae4 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -384,10 +384,14 @@ function decodeDirective( } function decodeDirectiveTuple( - directiveTuples: DirectiveTuple[], + directiveTuples: DirectiveTuple[] | undefined, types: TypeDefinitionsRecord, directives: DirectiveDefinitionTuple[], ): ReadonlyArray | undefined { + if (!directiveTuples) { + return; + } + return directiveTuples.map(([directiveName, args]) => { const directiveTuple = directives?.find( (directive) => getDirectiveName(directive) === directiveName, From 1066e1f939a8544dc0036a2fc6f1867df8cf1bc9 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:52:29 +0100 Subject: [PATCH 03/10] directive can be repeatable --- .../supermassive/src/schema/definition.ts | 8 ++++ .../src/utilities/decodeASTSchema.ts | 2 + .../src/utilities/encodeASTSchema.ts | 37 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 86cd7d4ca..b8c61c619 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -172,11 +172,13 @@ export type DirectiveDefinitionTuple = [ name: DirectiveName, locations: DirectiveLocation[], arguments?: InputValueDefinitionRecord, + repeatable?: boolean, ]; const enum DirectiveKeys { name = 0, locations = 1, arguments = 2, + repeatable = 3, } export type TypeDefinitionsRecord = Record; @@ -693,3 +695,9 @@ export function getDirectiveArguments( ): InputValueDefinitionRecord | undefined { return Array.isArray(def) ? def[DirectiveKeys.arguments] : undefined; } + +export function getDirectiveRepeatableKeyword( + def: DirectiveDefinitionTuple, +): boolean | undefined { + return Array.isArray(def) ? def[DirectiveKeys.repeatable] : undefined; +} diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index 6d812bae4..f6d671fbd 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -56,6 +56,7 @@ import { getInputTypeDirectives, getDirectiveArguments, DirectiveTuple, + getDirectiveRepeatableKeyword, } from "../schema/definition"; import { inspectTypeReference, @@ -407,6 +408,7 @@ function decodeDirectiveTuple( return { kind: Kind.DIRECTIVE, name: nameNode(directiveName), + repeatable: getDirectiveRepeatableKeyword(directiveTuple), arguments: args && argumentDefinitions ? Object.entries(args)?.map(([argName, argValue]) => { diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index 6dc052769..53fd023ed 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -51,7 +51,7 @@ export function encodeASTSchema( schemaFragment: DocumentNode, options?: EncodeASTSchemaOptions, ): SchemaDefinitions[] { - const includeDirectives = Boolean(options?.includeDirectives); + const includeDirectives = !options?.includeDirectives; const fragments: SchemaDefinitions[] = [{ types: {} }]; const add = (name: string, def: TypeDefinitionTuple, extension = false) => addTypeDefinition(fragments, name, def, extension); @@ -303,14 +303,35 @@ function encodeDirective( node: DirectiveDefinitionNode, ): DirectiveDefinitionTuple { if (node.arguments?.length) { - return [ - node.name.value, - node.locations.map((node) => - encodeDirectiveLocation(node.value as DirectiveLocationEnum), - ), - encodeArguments(node), - ]; + if (node.repeatable) { + return [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + encodeArguments(node), + node.repeatable, + ]; + } else { + return [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + encodeArguments(node), + ]; + } } else { + if (node.repeatable) { + [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + undefined, + node.repeatable || undefined, + ]; + } return [ node.name.value, node.locations.map((node) => From 98b9fe98291fc03bbd30b12c47ca4bde48e658be Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:09:34 +0100 Subject: [PATCH 04/10] repetable and directive improvements --- .../supermassive/src/schema/definition.ts | 7 + .../decodeASTSchema.test.ts.snap | 2 +- .../encodeASTSchema.test.ts.snap | 400 +----------------- .../__tests__/encodeASTSchema.test.ts | 5 - .../src/utilities/decodeASTSchema.ts | 76 ++-- .../src/utilities/encodeASTSchema.ts | 31 +- 6 files changed, 85 insertions(+), 436 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index b8c61c619..b443db93a 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -100,6 +100,7 @@ export type FieldDefinitionTuple = [ const enum FieldKeys { type = 0, arguments = 1, + directives = 2, } export type FieldDefinition = TypeReference | FieldDefinitionTuple; export type FieldDefinitionRecord = Record; @@ -529,6 +530,12 @@ export function getFieldArgs( return Array.isArray(field) ? field[FieldKeys.arguments] : undefined; } +export function getFieldDirectives( + field: FieldDefinition, +): DirectiveTuple[] | undefined { + return Array.isArray(field) ? field[FieldKeys.directives] : undefined; +} + export function setFieldArgs( field: FieldDefinitionTuple, args: InputValueDefinitionRecord, diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index 046ed2daa..44f5c1ea3 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap @@ -96,7 +96,7 @@ directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT -directive @myRepeatableDir(name: String!) on OBJECT | INTERFACE +directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE directive @onType on OBJECT diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index ca1839cba..1a409759b 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -46,405 +46,7 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` { "name": 6, }, - ], - [ - "onType", - [ - 11, - ], - ], - [ - "onObject", - [ - 11, - ], - { - "arg": 6, - }, - ], - [ - "onUnion", - [ - 15, - ], - ], - [ - "onInterface", - [ - 14, - ], - ], - [ - "onScalar", - [ - 10, - ], - ], - [ - "onEnum", - [ - 16, - ], - ], - [ - "onInputObject", - [ - 18, - ], - ], - ], - "types": { - "AnnotatedEnum": [ - 5, - [ - "ANNOTATED_VALUE", - "OTHER_VALUE", - ], - ], - "AnnotatedInput": [ - 6, - { - "annotatedField": "Type", - }, - ], - "AnnotatedInterface": [ - 3, - { - "annotatedField": [ - "Type", - { - "arg": "Type", - }, - ], - }, - ], - "AnnotatedObject": [ - 2, - { - "annotatedField": [ - "Type", - { - "arg": [ - "Type", - "default", - ], - }, - ], - }, - ], - "AnnotatedScalar": [ - 1, - ], - "AnnotatedUnion": [ - 4, - [ - "A", - "B", - ], - ], - "AnnotatedUnionTwo": [ - 4, - [ - "A", - "B", - ], - ], - "Bar": [ - 3, - { - "four": [ - 1, - { - "argument": [ - 1, - "string", - ], - }, - ], - "one": "Type", - }, - ], - "Baz": [ - 3, - { - "four": [ - 1, - { - "argument": [ - 1, - "string", - ], - }, - ], - "one": "Type", - "two": [ - "Type", - { - "argument": "InputType!", - }, - ], - }, - [ - "Bar", - "Two", - ], - ], - "CustomScalar": [ - 1, - ], - "Feed": [ - 4, - [ - "Story", - "Article", - "Advert", - ], - ], - "Foo": [ - 2, - { - "eight": [ - "Type", - { - "argument": "OneOfInputType", - }, - ], - "five": [ - 1, - { - "argument": [ - 11, - [ - "string", - "string", - ], - ], - }, - ], - "four": [ - 1, - { - "argument": [ - 1, - "string", - ], - }, - ], - "one": "Type", - "seven": [ - "Type", - { - "argument": [ - 3, - null, - ], - }, - ], - "six": [ - "Type", - { - "argument": [ - "InputType", - { - "key": "value", - }, - ], - }, - ], - "three": [ - 3, - { - "argument": "InputType", - "other": 1, - }, - ], - "two": [ - "Type", - { - "argument": "InputType!", - }, - ], - }, - [ - "Bar", - "Baz", - "Two", - ], - ], - "InputType": [ - 6, - { - "answer": [ - 3, - 42, - ], - "key": 6, - }, - ], - "OneOfInputType": [ - 6, - { - "int": 3, - "string": 1, - }, - ], - "Site": [ - 5, - [ - "DESKTOP", - "MOBILE", - "WEB", - ], - ], - "UndefinedEnum": [ - 5, - [], - ], - "UndefinedInput": [ - 6, - {}, - ], - "UndefinedInterface": [ - 3, - {}, - ], - "UndefinedType": [ - 2, - {}, - ], - "UndefinedUnion": [ - 4, - [], - ], - }, - }, - { - "types": { - "Bar": [ - 3, - { - "two": [ - "Type", - { - "argument": "InputType!", - }, - ], - }, - [ - "Two", - ], - ], - "CustomScalar": [ - 1, - ], - "Feed": [ - 4, - [ - "Photo", - "Video", - ], - ], - "Foo": [ - 2, - { - "seven": [ - "Type", - { - "argument": 11, - }, - ], - }, - ], - "InputType": [ - 6, - { - "other": [ - 4, - 12300, - ], - }, - ], - "Site": [ - 5, - [ - "VR", - ], - ], - }, - }, - { - "types": { - "Bar": [ - 3, - {}, - ], - "Feed": [ - 4, - [], - ], - "Foo": [ - 2, - {}, - ], - "InputType": [ - 6, - {}, - ], - "Site": [ - 5, - [], - ], - }, - }, -] -`; - -exports[`encodeASTSchema correctly encodes kitchen sink AST schema with directives 1`] = ` -[ - { - "directives": [ - [ - "skip", - [ - 4, - 6, - 7, - ], - { - "if": 7, - }, - ], - [ - "include", - [ - 4, - 6, - 7, - ], - { - "if": 7, - }, - ], - [ - "include2", - [ - 4, - 6, - 7, - ], - { - "if": 7, - }, - ], - [ - "myRepeatableDir", - [ - 11, - 14, - ], - { - "name": 6, - }, + true, ], [ "onType", diff --git a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts index 58b2c1b22..d4e31b3d1 100644 --- a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts @@ -12,9 +12,4 @@ describe(encodeASTSchema, () => { const encoded = encodeASTSchema(kitchenSinkSDL.document); expect(encoded).toMatchSnapshot(); }); - - test("correctly encodes kitchen sink AST schema with directives", () => { - const encoded = encodeASTSchema(kitchenSinkSDL.document); - expect(encoded).toMatchSnapshot(); - }); }); diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index f6d671fbd..b8763b89c 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -57,6 +57,7 @@ import { getDirectiveArguments, DirectiveTuple, getDirectiveRepeatableKeyword, + getFieldDirectives, } from "../schema/definition"; import { inspectTypeReference, @@ -123,12 +124,15 @@ function decodeScalarType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): ScalarTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getScalarTypeDirectives(tuple), + types, + directives, + ); return { kind: Kind.SCALAR_TYPE_DEFINITION, name: nameNode(typeName), - directives: - directives && - decodeDirectiveTuple(getScalarTypeDirectives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } @@ -138,6 +142,11 @@ function decodeEnumType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getEnumDirectives(tuple), + types, + directives, + ); return { kind: Kind.ENUM_TYPE_DEFINITION, name: nameNode(typeName), @@ -145,9 +154,7 @@ function decodeEnumType( kind: Kind.ENUM_VALUE_DEFINITION, name: nameNode(value), })), - directives: - directives && - decodeDirectiveTuple(getEnumDirectives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } @@ -157,17 +164,20 @@ function decodeObjectType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getObjectTypeDirectives(tuple), + types, + directives, + ); return { kind: Kind.OBJECT_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getObjectFields(tuple) ?? {}, types), + fields: decodeFields(getObjectFields(tuple) ?? {}, types, directives), interfaces: getObjectTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), })), - directives: - directives && - decodeDirectiveTuple(getObjectTypeDirectives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } @@ -177,6 +187,11 @@ function decodeInterfaceType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getInterfaceTypeDiretives(tuple), + types, + directives, + ); return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: nameNode(typeName), @@ -185,9 +200,7 @@ function decodeInterfaceType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), - directives: - directives && - decodeDirectiveTuple(getInterfaceTypeDiretives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } @@ -197,6 +210,11 @@ function decodeUnionType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getUnionTypeDirectives(tuple), + types, + directives, + ); return { kind: Kind.UNION_TYPE_DEFINITION, name: nameNode(typeName), @@ -204,9 +222,7 @@ function decodeUnionType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), - directives: - directives && - decodeDirectiveTuple(getUnionTypeDirectives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } @@ -216,29 +232,39 @@ function decodeInputObjectType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { + const decoded = decodeDirectiveTuple( + getInputTypeDirectives(tuple), + types, + directives, + ); return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, name: nameNode(typeName), fields: Object.entries(getInputObjectFields(tuple)).map(([name, value]) => decodeInputValue(name, value, types), ), - directives: - directives && - decodeDirectiveTuple(getInputTypeDirectives(tuple), types, directives), + ...(decoded && { directives: decoded }), }; } function decodeFields( fields: Record, types: TypeDefinitionsRecord, + directives?: DirectiveDefinitionTuple[], ): FieldDefinitionNode[] { return Object.entries(fields).map(([name, value]) => { const type = decodeTypeReference(getFieldTypeReference(value)); + const decoded = decodeDirectiveTuple( + getFieldDirectives(value), + types, + directives, + ); return { kind: Kind.FIELD_DEFINITION, name: nameNode(name), type, arguments: decodeArguments(getFieldArgs(value) ?? {}, types), + ...(decoded && { directives: decoded }), }; }); } @@ -379,17 +405,16 @@ function decodeDirective( kind: Kind.NAME, value: decodeDirectiveLocation(loc), })), - // TODO? repeatable are irrelevant for execution - repeatable: false, + repeatable: Boolean(getDirectiveRepeatableKeyword(tuple)), }; } function decodeDirectiveTuple( directiveTuples: DirectiveTuple[] | undefined, types: TypeDefinitionsRecord, - directives: DirectiveDefinitionTuple[], + directives?: DirectiveDefinitionTuple[], ): ReadonlyArray | undefined { - if (!directiveTuples) { + if (!directiveTuples || !directives) { return; } @@ -404,11 +429,12 @@ function decodeDirectiveTuple( ); const argumentDefinitions = getDirectiveArguments(directiveTuple); + const isRepeatable = Boolean(getDirectiveRepeatableKeyword(directiveTuple)); return { kind: Kind.DIRECTIVE, name: nameNode(directiveName), - repeatable: getDirectiveRepeatableKeyword(directiveTuple), + ...(isRepeatable && { isRepeatable }), arguments: args && argumentDefinitions ? Object.entries(args)?.map(([argName, argValue]) => { @@ -431,7 +457,7 @@ function decodeDirectiveTuple( ), }; }) - : undefined, + : [], }; }); } diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index 53fd023ed..e859c5524 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -51,7 +51,7 @@ export function encodeASTSchema( schemaFragment: DocumentNode, options?: EncodeASTSchemaOptions, ): SchemaDefinitions[] { - const includeDirectives = !options?.includeDirectives; + const includeDirectives = Boolean(options?.includeDirectives); const fragments: SchemaDefinitions[] = [{ types: {} }]; const add = (name: string, def: TypeDefinitionTuple, extension = false) => addTypeDefinition(fragments, name, def, extension); @@ -185,7 +185,7 @@ function encodeObjectType( ): ObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field); + fields[field.name.value] = encodeField(field, includeDirectives); } return createObjectTypeDefinition( fields, @@ -224,7 +224,7 @@ function encodeInterfaceType( ): InterfaceTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field); + fields[field.name.value] = encodeField(field, includeDirectives); } return createInterfaceTypeDefinition( fields, @@ -271,10 +271,29 @@ function encodeInputObjectType( function encodeField( node: FieldDefinitionNode, + includeDirectives: boolean, ): TypeReference | FieldDefinitionTuple { - return !node.arguments?.length - ? typeReferenceFromNode(node.type) - : [typeReferenceFromNode(node.type), encodeArguments(node)]; + if (!node.arguments?.length) { + return typeReferenceFromNode(node.type); + } + + if (includeDirectives && node.directives?.length) { + console.log( + node.name, + node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + ); + return [ + typeReferenceFromNode(node.type), + encodeArguments(node), + node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + ]; + } + + return [typeReferenceFromNode(node.type), encodeArguments(node)]; } function encodeArguments( From 86568c74d1d8c34bee394ec4fc7f2f21651a8c29 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:46:45 +0100 Subject: [PATCH 05/10] public pre refactoring tests --- .../supermassive/src/schema/definition.ts | 59 +++- .../decodeASTSchema.test.ts.snap | 296 ++++++++++++++++++ .../encodeASTSchema.test.ts.snap | 12 + .../__tests__/decodeASTSchema.test.ts | 27 ++ .../__tests__/fixtures/kitchenSinkSDL.ts | 4 + .../__tests__/mergeSchemaDefinitions.test.ts | 161 +++++++++- .../src/utilities/decodeASTSchema.ts | 2 +- .../src/utilities/encodeASTSchema.ts | 27 +- .../src/utilities/mergeSchemaDefinitions.ts | 39 ++- 9 files changed, 604 insertions(+), 23 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index b443db93a..43489d638 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -25,11 +25,22 @@ const enum ScalarKeys { directives = 1, } +// type TypeDefinitionMetadata = { +// directives?: DirectiveTuple[]; +// description?: string; +// }; + +// type DirectiveDefinitionMetadata = { +// isRepeatable?: boolean; +// description?: string; +// }; + export type ObjectTypeDefinitionTuple = [ kind: TypeKind.OBJECT, fields: FieldDefinitionRecord, interfaces?: TypeName[], directives?: DirectiveTuple[], + //metadata?: TypeDefinitionMetadata, ]; const enum ObjectKeys { fields = 1, @@ -94,7 +105,8 @@ export type CompositeTypeTuple = export type FieldDefinitionTuple = [ type: TypeReference, - arguments: InputValueDefinitionRecord, + // TODO should I really do it ? + arguments?: InputValueDefinitionRecord, directives?: DirectiveTuple[], ]; const enum FieldKeys { @@ -165,6 +177,7 @@ const DirectiveLocationToGraphQL = { } as const; export type DirectiveName = string; + export type DirectiveTuple = [ name: DirectiveName, arguments?: Record, // JS values (cannot be a variable inside schema definition, so it is fine) @@ -200,6 +213,42 @@ export type SchemaDefinitions = { const typeNameMetaFieldDef: FieldDefinition = "String"; const specifiedScalarDefinition: ScalarTypeDefinitionTuple = [TypeKind.SCALAR]; +export function getTypeDefinitionDirectiveIndex( + typeDefinition: TypeDefinitionTuple, +): number | undefined { + if (isObjectTypeDefinition(typeDefinition)) { + return ObjectKeys.directives; + } else if (isScalarTypeDefinition(typeDefinition)) { + return ScalarKeys.directives; + } else if (isEnumTypeDefinition(typeDefinition)) { + return EnumKeys.directives; + } else if (isInterfaceTypeDefinition(typeDefinition)) { + return InterfaceKeys.directives; + } else if (isInputObjectTypeDefinition(typeDefinition)) { + return InputObjectKeys.directives; + } else if (isUnionTypeDefinition(typeDefinition)) { + return UnionKeys.directives; + } +} + +export function getTypeDefinitionDirectives( + typeDefinition: TypeDefinitionTuple, +) { + if (isObjectTypeDefinition(typeDefinition)) { + return getObjectTypeDirectives(typeDefinition); + } else if (isScalarTypeDefinition(typeDefinition)) { + return getScalarTypeDirectives(typeDefinition); + } else if (isEnumTypeDefinition(typeDefinition)) { + return getEnumDirectives(typeDefinition); + } else if (isInterfaceTypeDefinition(typeDefinition)) { + return getInterfaceTypeDiretives(typeDefinition); + } else if (isInputObjectTypeDefinition(typeDefinition)) { + return getInputTypeDirectives(typeDefinition); + } else if (isUnionTypeDefinition(typeDefinition)) { + return getUnionTypeDirectives(typeDefinition); + } +} + export function findObjectType( defs: SchemaDefinitions, typeName: TypeName, @@ -544,6 +593,14 @@ export function setFieldArgs( return args; } +export function setFieldDirectives( + field: FieldDefinitionTuple, + args: DirectiveTuple[], +): DirectiveTuple[] { + field[FieldKeys.directives] = args; + return args; +} + export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { return tuple[EnumKeys.values]; } diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index 44f5c1ea3..55800d5fc 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap @@ -104,12 +104,122 @@ directive @onObject(arg: String!) on OBJECT directive @onUnion on UNION +directive @onField on FIELD + directive @onInterface on INTERFACE directive @onScalar on SCALAR directive @onEnum on ENUM +directive @oneOf on INPUT_OBJECT + +directive @onInputObject on INPUT_OBJECT +" +`; + +exports[`decodeASTSchema correctly encodes kitchen sink AST schema with directives 1`] = ` +"type Foo implements Bar & Baz & Two @onType { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type +} + +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = default): Type @onField +} + +type UndefinedType + +interface Bar implements Two @onInterface { + one: Type + four(argument: String = "string"): String + two(argument: InputType!): Type +} + +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type): Type @onField +} + +interface UndefinedInterface + +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + +union Feed @onUnion = Story | Article | Advert + +union AnnotatedUnion @onUnion = A | B + +union AnnotatedUnionTwo @onUnion = A | B + +union UndefinedUnion + +scalar CustomScalar @onScalar + +scalar AnnotatedScalar @onScalar + +enum Site @onEnum { + DESKTOP + MOBILE + WEB +} + +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE + OTHER_VALUE +} + +enum UndefinedEnum + +input InputType @onInputObject { + key: String! + answer: Int = 42 + other: Float = 12300 +} + +input OneOfInputType @oneOf { + string: String + int: Int +} + +input AnnotatedInput @onInputObject { + annotatedField: Type +} + +input UndefinedInput + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE + +directive @onType on OBJECT + +directive @onObject(arg: String!) on OBJECT + +directive @onUnion on UNION + +directive @onField on FIELD + +directive @onInterface on INTERFACE + +directive @onScalar on SCALAR + +directive @onEnum on ENUM + +directive @oneOf on INPUT_OBJECT + directive @onInputObject on INPUT_OBJECT " `; @@ -299,3 +409,189 @@ input AdvancedInput { directive @i18n(locale: String) on QUERY " `; + +exports[`decodeASTSchema correctly encodes swapi AST schema with directives 1`] = ` +"union SearchResult = Person | Starship | Transport | Species | Vehicle | Planet | Film + +enum NodeType { + Person + Starship + Transport + Species + Vehicle + Planet + Film +} + +type Subscription { + emitPersons(limit: Int!, throwError: Boolean): Person +} + +type Query { + node(nodeType: NodeType!, id: Int!): Node + search(search: String): [SearchResult] + person(id: Int!): Person + planet(id: Int!): Planet + film(id: Int!): Film + transport(id: Int!): Transport + starship(id: Int!): Starship + vehicle(id: Int!): Vehicle + searchPeopleByName(search: String!): [Person] + searchStarshipsByName(search: String!): [Starship] + searchTransportsByName(search: String!): [Transport] + searchSpeciesByName(search: String!): [Species] + searchVehiclesByName(search: String!): [Vehicle] + searchPlanetsByName(search: String!): [Planet] + searchFilmsByTitle(search: String! = "The Empire Strikes Back"): [Film] + allFilms: [Film] + allStarships: [Starship] + allPeople: [Person] + allPlanets: [Planet] + allSpecies: [Species] + allTransports: [Transport] + advancedDefaultInput(input: AdvancedInput! = {enumField: Transport, otherField: "Foo"}): String + multiArger(a: Int, b: String, c: AdvancedInput): String +} + +interface Node { + id: Int +} + +union Alive = Person | Species + +type Film implements Node { + title: String! + starships: [Starship] + edited: String + vehicles: [Vehicle] + planets: [Planet] + producer: String + created: String + episode_id: Int + director: String + release_date: String + opening_crawl: String + characters: [Person] + species: [Species] + id: Int +} + +type Vehicle implements Node { + id: Int + name: String + vehicle_class: String + pilots: [Person] + edited: String + consumables: String + created: String + model: String + manufacturer: String + image: String + cargo_capacity: Int + passengers: Int + max_atmosphering_speed: Int + crew: Int + length: Float + cost_in_credits: Int +} + +type Person implements Node { + id: Int + edited: String + name: String + created: String + gender: String + skin_color: String + hair_color: String + height: Int + eye_color: String + mass: Int + homeworld: Planet + birth_year: String + image: String + vehicles: [Vehicle] + starships: [Starship] + films: [Film] +} + +type Starship implements Node { + id: Int + films: [Film] + pilots: [Person] + MGLT: Int + starship_class: String + hyperdrive_rating: Float + edited: String + consumables: String + name: String + created: String + cargo_capacity: Int + passengers: Int + max_atmosphering_speed: Int + crew: String + length: Int + model: String + cost_in_credits: Int + manufacturer: String + image: String +} + +type Planet implements Node { + id: Int + edited: String + climate: String + surface_water: String + name: String + diameter: Int + rotation_period: Int + created: String + terrain: String + gravity: String + orbital_period: Int + population: Int + residents: [Person] + films: [Film] +} + +type Species implements Node { + edited: String + classification: String + name: String + designation: String + created: String + eye_colors: String + people: [Person] + skin_colors: String + language: String + hair_colors: String + homeworld: Planet + average_lifespan: Int + average_height: Int + id: Int +} + +type Transport implements Node { + edited: String + consumables: String + name: String + created: String + cargo_capacity: Int + passengers: Int + max_atmosphering_speed: Int + crew: String + length: Int + model: String + cost_in_credits: Int + manufacturer: String + image: String + id: Int +} + +input AdvancedInput { + enumField: NodeType! + otherField: String! +} + +directive @i18n(locale: String) on QUERY +" +`; diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index 1a409759b..1161bf54b 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -69,6 +69,12 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` 15, ], ], + [ + "onField", + [ + 4, + ], + ], [ "onInterface", [ @@ -87,6 +93,12 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` 16, ], ], + [ + "oneOf", + [ + 18, + ], + ], [ "onInputObject", [ diff --git a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts index 9a59fbaa5..e2e0f4246 100644 --- a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts @@ -30,6 +30,33 @@ describe(decodeASTSchema, () => { expect(print(decoded)).toMatchSnapshot(); }); + test("correctly encodes swapi AST schema with directives", () => { + const doc = cleanUpDocument(swapiSDL.document); + const encoded = encodeASTSchema(doc, { includeDirectives: true }); + const decoded = decodeASTSchema(encoded); + + expect(decoded).toEqual(doc); + expect(encodeASTSchema(decoded, { includeDirectives: true })).toEqual( + encoded, + ); + expect(print(decoded)).toMatchSnapshot(); + }); + + test("correctly encodes kitchen sink AST schema with directives", () => { + const doc = cleanUpDocument(kitchenSinkSDL.document); + const encoded = [ + mergeSchemaDefinitions( + { types: {}, directives: [] }, + encodeASTSchema(doc, { includeDirectives: true }), + ), + ]; + const decoded = decodeASTSchema(encoded); + expect(encodeASTSchema(decoded, { includeDirectives: true })).toEqual( + encoded, + ); + expect(print(decoded)).toMatchSnapshot(); + }); + test("correctly encodes a schema with a Boolean parameter", () => { const doc = cleanUpDocument(schemaWithBooleanParameter.document); const encoded = encodeASTSchema(doc); diff --git a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts index e16751f4a..c309aeaab 100644 --- a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts +++ b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts @@ -161,12 +161,16 @@ export const kitchenSinkSDL = gql` directive @onUnion on UNION + directive @onField on FIELD + directive @onInterface on INTERFACE directive @onScalar on SCALAR directive @onEnum on ENUM + directive @oneOf on INPUT_OBJECT + directive @onInputObject on INPUT_OBJECT extend schema @onSchema diff --git a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts index c9a231213..da7c3c1ff 100644 --- a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts +++ b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts @@ -3,12 +3,18 @@ import { createSchemaDefinitions, mergeSchemaDefinitions, } from "../mergeSchemaDefinitions"; -import { encodeASTSchema } from "../encodeASTSchema"; +import { + encodeASTSchema, + type EncodeASTSchemaOptions, +} from "../encodeASTSchema"; import { SchemaDefinitions } from "../../schema/definition"; -function schema(sdl: string): SchemaDefinitions[] { +function schema( + sdl: string, + options?: EncodeASTSchemaOptions, +): SchemaDefinitions[] { const doc = parse(sdl); - return encodeASTSchema(doc); + return encodeASTSchema(doc, options); } describe("mergeSchemaDefinitions", () => { @@ -322,6 +328,155 @@ describe("mergeSchemaDefinitions", () => { `); }); + it("merge directives from source to target", () => { + const defs = schema( + ` +extend type Query { + user24: String! @context + } + `, + { includeDirectives: true }, + ); + + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Query": [ + 2, + { + "user24": [ + 6, + undefined, + [ + [ + "context", + ], + ], + ], + }, + ], + }, + } + `); + }); + it("merge directives from source to target", () => { + const defs = schema( + ` +interface IUser @onInterface { + id: ID! @onField +} + +type User implements IUser @onType { + id: ID! @onField +} + +extend type Query { + user( + id: String! + ): User @context + } + `, + { includeDirectives: true }, + ); + const defs2 = schema( + ` +extend interface IUser @onExtendInterface { + name: String! +} + +extend type User implements IUser @onExtendType { + name: String! +} +extend type Query { + user( + id: String! + ): String @oneOf + } + `, + { includeDirectives: true }, + ); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs2); + const finalResult = mergeSchemaDefinitions(result, defs); + expect(finalResult).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "IUser": [ + 3, + { + "id": [ + 10, + undefined, + [ + [ + "onField", + ], + ], + ], + "name": 6, + }, + [], + [ + [ + "onExtendInterface", + ], + [ + "onInterface", + ], + ], + ], + "Query": [ + 2, + { + "user": [ + 1, + { + "id": 6, + }, + [ + [ + "oneOf", + ], + [ + "context", + ], + ], + ], + }, + ], + "User": [ + 2, + { + "id": [ + 10, + undefined, + [ + [ + "onField", + ], + ], + ], + "name": 6, + }, + [ + "IUser", + ], + [ + [ + "onExtendType", + ], + [ + "onType", + ], + ], + ], + }, + } + `); + }); + it("should add unique interfaces from source to target", () => { const defs = schema(` type User implements Node { diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index b8763b89c..6086dd215 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -195,7 +195,7 @@ function decodeInterfaceType( return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getFields(tuple), types), + fields: decodeFields(getFields(tuple), types, directives), interfaces: getInterfaceTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index e859c5524..117246dad 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -43,7 +43,7 @@ import { import { typeReferenceFromNode, TypeReference } from "../schema/reference"; import { valueFromASTUntyped } from "./valueFromASTUntyped"; -type EncodeASTSchemaOptions = { +export type EncodeASTSchemaOptions = { includeDirectives?: boolean; }; @@ -273,26 +273,29 @@ function encodeField( node: FieldDefinitionNode, includeDirectives: boolean, ): TypeReference | FieldDefinitionTuple { + let directiveTuples: DirectiveTuple[] | undefined; + + if (includeDirectives && node.directives?.length) { + directiveTuples = node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive); + } + if (!node.arguments?.length) { + if (directiveTuples) { + return [typeReferenceFromNode(node.type), undefined, directiveTuples]; + } + return typeReferenceFromNode(node.type); } - if (includeDirectives && node.directives?.length) { - console.log( - node.name, - node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - ); + if (directiveTuples) { return [ typeReferenceFromNode(node.type), encodeArguments(node), - node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), + directiveTuples, ]; } - return [typeReferenceFromNode(node.type), encodeArguments(node)]; } diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index ea1f95943..29ffc62b6 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -5,8 +5,11 @@ import { getDirectiveDefinitionArgs, getDirectiveName, getFieldArgs, + getFieldDirectives, getFields, getInputObjectFields, + getTypeDefinitionDirectiveIndex, + getTypeDefinitionDirectives, InputValueDefinitionRecord, InterfaceTypeDefinitionTuple, isInputObjectTypeDefinition, @@ -16,6 +19,7 @@ import { SchemaDefinitions, setDirectiveDefinitionArgs, setFieldArgs, + setFieldDirectives, TypeDefinitionsRecord, TypeDefinitionTuple, } from "../schema/definition"; @@ -47,19 +51,22 @@ export function mergeSchemaDefinitions( return accumulator; } -function mergeFieldDirectives( +function mergeTypeDirectives( target: TypeDefinitionTuple, source: TypeDefinitionTuple, ): void { - const targetDirectives: DirectiveTuple[] | undefined = target[3]; - const sourceDirectives: DirectiveTuple[] | undefined = source[3]; + const targetDirectives: DirectiveTuple[] | undefined = + getTypeDefinitionDirectives(target); + const sourceDirectives: DirectiveTuple[] | undefined = + getTypeDefinitionDirectives(source); - if (!sourceDirectives) { + const directiveIndex = getTypeDefinitionDirectiveIndex(target); + if (!sourceDirectives || !directiveIndex) { return; } if (!targetDirectives) { - target[3] = [...sourceDirectives]; + target[directiveIndex] = [...sourceDirectives]; return; } @@ -114,7 +121,7 @@ export function mergeTypes( continue; } - mergeFieldDirectives(targetDef, sourceDef); + mergeTypeDirectives(targetDef, sourceDef); if ( (isObjectTypeDefinition(targetDef) && @@ -163,6 +170,26 @@ function mergeFields( const targetArgs = getFieldArgs(targetDef) ?? setFieldArgs(targetDef, {}); mergeInputValues(targetArgs, sourceArgs); } + + const sourceDirectives = getFieldDirectives(sourceDef); + if (sourceDirectives) { + const targetDirectives = + getFieldDirectives(targetDef) ?? setFieldDirectives(targetDef, []); + mergeFieldDirectives(targetDirectives, sourceDirectives); + } + } +} + +function mergeFieldDirectives( + target: DirectiveTuple[], + source: DirectiveTuple[], +): void { + for (const sourceDirective of source) { + const directiveName = sourceDirective[0]; + const exists = target.some((d: DirectiveTuple) => d[0] === directiveName); + if (!exists) { + target.push(sourceDirective); + } } } From 899d60b6ba902d4765a1de690078f2f826d62f96 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:54:31 +0100 Subject: [PATCH 06/10] metadata re-work --- .../supermassive/src/schema/definition.ts | 175 +++++++++--------- .../encodeASTSchema.test.ts.snap | 4 +- .../__tests__/mergeSchemaDefinitions.test.ts | 78 ++++---- .../src/utilities/decodeASTSchema.ts | 36 ++-- .../src/utilities/encodeASTSchema.ts | 117 +++++++----- .../src/utilities/mergeSchemaDefinitions.ts | 63 ++++--- 6 files changed, 265 insertions(+), 208 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 43489d638..0010a85ae 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -18,76 +18,75 @@ const enum TypeKind { export type ScalarTypeDefinitionTuple = [ kind: TypeKind.SCALAR, - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum ScalarKeys { - directives = 1, + metadata = 1, } -// type TypeDefinitionMetadata = { -// directives?: DirectiveTuple[]; -// description?: string; -// }; +export type TypeDefinitionMetadata = { + directives?: DirectiveTuple[]; + description?: string; +}; -// type DirectiveDefinitionMetadata = { -// isRepeatable?: boolean; -// description?: string; -// }; +export type DirectiveDefinitionMetadata = { + isRepeatable?: boolean; + description?: string; +}; export type ObjectTypeDefinitionTuple = [ kind: TypeKind.OBJECT, fields: FieldDefinitionRecord, interfaces?: TypeName[], - directives?: DirectiveTuple[], - //metadata?: TypeDefinitionMetadata, + metadata?: TypeDefinitionMetadata, ]; const enum ObjectKeys { fields = 1, interfaces = 2, - directives = 3, + metadata = 3, } export type InterfaceTypeDefinitionTuple = [ kind: TypeKind.INTERFACE, fields: FieldDefinitionRecord, interfaces?: TypeName[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum InterfaceKeys { fields = 1, interfaces = 2, - directives = 3, + metadata = 3, } export type UnionTypeDefinitionTuple = [ kind: TypeKind.UNION, types: TypeName[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum UnionKeys { types = 1, - directives = 2, + metadata = 2, } export type EnumTypeDefinitionTuple = [ kind: TypeKind.ENUM, values: string[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum EnumKeys { values = 1, - directives = 2, + metadata = 2, } export type InputObjectTypeDefinitionTuple = [ kind: TypeKind.INPUT, fields: InputValueDefinitionRecord, - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum InputObjectKeys { fields = 1, - directives = 2, + metadata = 2, } export type TypeDefinitionTuple = @@ -107,12 +106,12 @@ export type FieldDefinitionTuple = [ type: TypeReference, // TODO should I really do it ? arguments?: InputValueDefinitionRecord, - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum FieldKeys { type = 0, arguments = 1, - directives = 2, + metadata = 2, } export type FieldDefinition = TypeReference | FieldDefinitionTuple; export type FieldDefinitionRecord = Record; @@ -120,7 +119,7 @@ export type FieldDefinitionRecord = Record; export type InputValueDefinitionTuple = [ type: TypeReference, defaultValue: unknown, - directives?: DirectiveTuple[], + metadata?: DirectiveDefinitionMetadata, ]; const enum InputValueKeys { type = 0, @@ -186,13 +185,13 @@ export type DirectiveDefinitionTuple = [ name: DirectiveName, locations: DirectiveLocation[], arguments?: InputValueDefinitionRecord, - repeatable?: boolean, + metadata?: DirectiveDefinitionMetadata, ]; const enum DirectiveKeys { name = 0, locations = 1, arguments = 2, - repeatable = 3, + metadata = 3, } export type TypeDefinitionsRecord = Record; @@ -213,39 +212,37 @@ export type SchemaDefinitions = { const typeNameMetaFieldDef: FieldDefinition = "String"; const specifiedScalarDefinition: ScalarTypeDefinitionTuple = [TypeKind.SCALAR]; -export function getTypeDefinitionDirectiveIndex( +export function getTypeDefinitionMetadataIndex( typeDefinition: TypeDefinitionTuple, ): number | undefined { if (isObjectTypeDefinition(typeDefinition)) { - return ObjectKeys.directives; + return ObjectKeys.metadata; } else if (isScalarTypeDefinition(typeDefinition)) { - return ScalarKeys.directives; + return ScalarKeys.metadata; } else if (isEnumTypeDefinition(typeDefinition)) { - return EnumKeys.directives; + return EnumKeys.metadata; } else if (isInterfaceTypeDefinition(typeDefinition)) { - return InterfaceKeys.directives; + return InterfaceKeys.metadata; } else if (isInputObjectTypeDefinition(typeDefinition)) { - return InputObjectKeys.directives; + return InputObjectKeys.metadata; } else if (isUnionTypeDefinition(typeDefinition)) { - return UnionKeys.directives; + return UnionKeys.metadata; } } -export function getTypeDefinitionDirectives( - typeDefinition: TypeDefinitionTuple, -) { +export function getTypeDefinitionMetadata(typeDefinition: TypeDefinitionTuple) { if (isObjectTypeDefinition(typeDefinition)) { - return getObjectTypeDirectives(typeDefinition); + return getObjectTypeMetadata(typeDefinition); } else if (isScalarTypeDefinition(typeDefinition)) { - return getScalarTypeDirectives(typeDefinition); + return getScalarTypeMetadata(typeDefinition); } else if (isEnumTypeDefinition(typeDefinition)) { - return getEnumDirectives(typeDefinition); + return getEnumMetadata(typeDefinition); } else if (isInterfaceTypeDefinition(typeDefinition)) { - return getInterfaceTypeDiretives(typeDefinition); + return getInterfaceTypeMetadata(typeDefinition); } else if (isInputObjectTypeDefinition(typeDefinition)) { - return getInputTypeDirectives(typeDefinition); + return getInputTypeMetadata(typeDefinition); } else if (isUnionTypeDefinition(typeDefinition)) { - return getUnionTypeDirectives(typeDefinition); + return getUnionTypeMetadata(typeDefinition); } } @@ -579,10 +576,10 @@ export function getFieldArgs( return Array.isArray(field) ? field[FieldKeys.arguments] : undefined; } -export function getFieldDirectives( +export function getFieldMetadata( field: FieldDefinition, -): DirectiveTuple[] | undefined { - return Array.isArray(field) ? field[FieldKeys.directives] : undefined; +): TypeDefinitionMetadata | undefined { + return Array.isArray(field) ? field[FieldKeys.metadata] : undefined; } export function setFieldArgs( @@ -595,9 +592,9 @@ export function setFieldArgs( export function setFieldDirectives( field: FieldDefinitionTuple, - args: DirectiveTuple[], -): DirectiveTuple[] { - field[FieldKeys.directives] = args; + args: TypeDefinitionMetadata, +): TypeDefinitionMetadata { + field[FieldKeys.metadata] = args; return args; } @@ -605,10 +602,10 @@ export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { return tuple[EnumKeys.values]; } -export function getEnumDirectives( +export function getEnumMetadata( tuple: EnumTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return tuple[EnumKeys.directives]; +): TypeDefinitionMetadata | undefined { + return tuple[EnumKeys.metadata]; } export function getDirectiveDefinitionArgs( @@ -627,10 +624,10 @@ export function setDirectiveDefinitionArgs( export function createUnionTypeDefinition( types: TypeName[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): UnionTypeDefinitionTuple { - if (directives?.length) { - return [TypeKind.UNION, types, directives]; + if (metadata) { + return [TypeKind.UNION, types, metadata]; } return [TypeKind.UNION, types]; @@ -639,41 +636,41 @@ export function createUnionTypeDefinition( export function createInterfaceTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): InterfaceTypeDefinitionTuple { - if (!interfaces?.length && !directives?.length) { + if (!interfaces?.length && !metadata) { return [TypeKind.INTERFACE, fields]; } - if (interfaces?.length && !directives?.length) { + if (interfaces?.length && !metadata) { return [TypeKind.INTERFACE, fields, interfaces]; } - return [TypeKind.INTERFACE, fields, interfaces, directives]; + return [TypeKind.INTERFACE, fields, interfaces, metadata]; } export function createObjectTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): ObjectTypeDefinitionTuple { - if (!interfaces?.length && !directives?.length) { + if (!interfaces?.length && !metadata) { return [TypeKind.OBJECT, fields]; } - if (interfaces?.length && !directives?.length) { + if (interfaces?.length && !metadata) { return [TypeKind.OBJECT, fields, interfaces]; } - return [TypeKind.OBJECT, fields, interfaces, directives]; + return [TypeKind.OBJECT, fields, interfaces, metadata]; } export function createInputObjectTypeDefinition( fields: InputValueDefinitionRecord, - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): InputObjectTypeDefinitionTuple { - if (directives?.length) { - return [TypeKind.INPUT, fields, directives]; + if (metadata) { + return [TypeKind.INPUT, fields, metadata]; } return [TypeKind.INPUT, fields]; @@ -681,35 +678,35 @@ export function createInputObjectTypeDefinition( export function createEnumTypeDefinition( values: string[], - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): EnumTypeDefinitionTuple { - if (directives?.length) { - return [TypeKind.ENUM, values, directives]; + if (metadata) { + return [TypeKind.ENUM, values, metadata]; } return [TypeKind.ENUM, values]; } export function createScalarTypeDefinition( - directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ): ScalarTypeDefinitionTuple { - if (directives?.length) { - return [TypeKind.SCALAR, directives]; + if (metadata) { + return [TypeKind.SCALAR, metadata]; } return [TypeKind.SCALAR]; } -export function getScalarTypeDirectives( +export function getScalarTypeMetadata( def: ScalarTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return def[ScalarKeys.directives]; +): TypeDefinitionMetadata | undefined { + return def[ScalarKeys.metadata]; } -export function getObjectTypeDirectives( +export function getObjectTypeMetadata( def: ObjectTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return def[ObjectKeys.directives]; +): TypeDefinitionMetadata | undefined { + return def[ObjectKeys.metadata]; } export function getObjectTypeInterfaces( @@ -724,16 +721,16 @@ export function getInterfaceTypeInterfaces( return def[InterfaceKeys.interfaces] ?? []; } -export function getInterfaceTypeDiretives( +export function getInterfaceTypeMetadata( def: InterfaceTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return def[InterfaceKeys.directives]; +): TypeDefinitionMetadata | undefined { + return def[InterfaceKeys.metadata]; } -export function getInputTypeDirectives( +export function getInputTypeMetadata( def: InputObjectTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return def[InputObjectKeys.directives]; +): TypeDefinitionMetadata | undefined { + return def[InputObjectKeys.metadata]; } export function getUnionTypeMembers( @@ -742,10 +739,10 @@ export function getUnionTypeMembers( return tuple[UnionKeys.types]; } -export function getUnionTypeDirectives( +export function getUnionTypeMetadata( def: UnionTypeDefinitionTuple, -): DirectiveTuple[] | undefined { - return def[UnionKeys.directives]; +): TypeDefinitionMetadata | undefined { + return def[UnionKeys.metadata]; } export function getFieldArguments( @@ -760,8 +757,8 @@ export function getDirectiveArguments( return Array.isArray(def) ? def[DirectiveKeys.arguments] : undefined; } -export function getDirectiveRepeatableKeyword( +export function getDirectiveMetadata( def: DirectiveDefinitionTuple, -): boolean | undefined { - return Array.isArray(def) ? def[DirectiveKeys.repeatable] : undefined; +): DirectiveDefinitionMetadata | undefined { + return Array.isArray(def) ? def[DirectiveKeys.metadata] : undefined; } diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index 1161bf54b..dd82f07c4 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -46,7 +46,9 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` { "name": 6, }, - true, + { + "isRepeatable": true, + }, ], [ "onType", diff --git a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts index da7c3c1ff..cc0a93fa1 100644 --- a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts +++ b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts @@ -349,11 +349,13 @@ extend type Query { "user24": [ 6, undefined, - [ - [ - "context", + { + "directives": [ + [ + "context", + ], ], - ], + }, ], }, ], @@ -409,23 +411,27 @@ extend type Query { "id": [ 10, undefined, - [ - [ - "onField", + { + "directives": [ + [ + "onField", + ], ], - ], + }, ], "name": 6, }, [], - [ - [ - "onExtendInterface", - ], - [ - "onInterface", + { + "directives": [ + [ + "onExtendInterface", + ], + [ + "onInterface", + ], ], - ], + }, ], "Query": [ 2, @@ -435,14 +441,16 @@ extend type Query { { "id": 6, }, - [ - [ - "oneOf", - ], - [ - "context", + { + "directives": [ + [ + "oneOf", + ], + [ + "context", + ], ], - ], + }, ], }, ], @@ -452,25 +460,29 @@ extend type Query { "id": [ 10, undefined, - [ - [ - "onField", + { + "directives": [ + [ + "onField", + ], ], - ], + }, ], "name": 6, }, [ "IUser", ], - [ - [ - "onExtendType", - ], - [ - "onType", + { + "directives": [ + [ + "onExtendType", + ], + [ + "onType", + ], ], - ], + }, ], }, } diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index 6086dd215..d7cd00eef 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -47,17 +47,17 @@ import { getDirectiveDefinitionArgs, getDirectiveLocations, decodeDirectiveLocation, - getObjectTypeDirectives, - getInterfaceTypeDiretives, - getEnumDirectives, - getUnionTypeDirectives, + getObjectTypeMetadata, + getInterfaceTypeMetadata, + getEnumMetadata, + getUnionTypeMetadata, ScalarTypeDefinitionTuple, - getScalarTypeDirectives, - getInputTypeDirectives, + getScalarTypeMetadata, + getInputTypeMetadata, getDirectiveArguments, DirectiveTuple, - getDirectiveRepeatableKeyword, - getFieldDirectives, + getDirectiveMetadata, + getFieldMetadata, } from "../schema/definition"; import { inspectTypeReference, @@ -125,7 +125,7 @@ function decodeScalarType( directives?: DirectiveDefinitionTuple[], ): ScalarTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getScalarTypeDirectives(tuple), + getScalarTypeMetadata(tuple)?.directives, types, directives, ); @@ -143,7 +143,7 @@ function decodeEnumType( directives?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getEnumDirectives(tuple), + getEnumMetadata(tuple)?.directives, types, directives, ); @@ -165,7 +165,7 @@ function decodeObjectType( directives?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getObjectTypeDirectives(tuple), + getObjectTypeMetadata(tuple)?.directives, types, directives, ); @@ -188,7 +188,7 @@ function decodeInterfaceType( directives?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getInterfaceTypeDiretives(tuple), + getInterfaceTypeMetadata(tuple)?.directives, types, directives, ); @@ -211,7 +211,7 @@ function decodeUnionType( directives?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getUnionTypeDirectives(tuple), + getUnionTypeMetadata(tuple)?.directives, types, directives, ); @@ -233,7 +233,7 @@ function decodeInputObjectType( directives?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { const decoded = decodeDirectiveTuple( - getInputTypeDirectives(tuple), + getInputTypeMetadata(tuple)?.directives, types, directives, ); @@ -255,7 +255,7 @@ function decodeFields( return Object.entries(fields).map(([name, value]) => { const type = decodeTypeReference(getFieldTypeReference(value)); const decoded = decodeDirectiveTuple( - getFieldDirectives(value), + getFieldMetadata(value)?.directives, types, directives, ); @@ -405,7 +405,7 @@ function decodeDirective( kind: Kind.NAME, value: decodeDirectiveLocation(loc), })), - repeatable: Boolean(getDirectiveRepeatableKeyword(tuple)), + repeatable: Boolean(getDirectiveMetadata(tuple)?.isRepeatable), }; } @@ -429,7 +429,9 @@ function decodeDirectiveTuple( ); const argumentDefinitions = getDirectiveArguments(directiveTuple); - const isRepeatable = Boolean(getDirectiveRepeatableKeyword(directiveTuple)); + const isRepeatable = Boolean( + getDirectiveMetadata(directiveTuple)?.isRepeatable, + ); return { kind: Kind.DIRECTIVE, diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index 117246dad..6f744498f 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -39,6 +39,7 @@ import { createUnionTypeDefinition, encodeDirectiveLocation, DirectiveTuple, + TypeDefinitionMetadata, } from "../schema/definition"; import { typeReferenceFromNode, TypeReference } from "../schema/reference"; import { valueFromASTUntyped } from "./valueFromASTUntyped"; @@ -156,26 +157,34 @@ function encodeScalarType( type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, includeDirectives: boolean, ): ScalarTypeDefinitionTuple { - return createScalarTypeDefinition( - includeDirectives - ? type.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, - ); + const metadata = + includeDirectives && type.directives + ? { + directives: type.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; + + return createScalarTypeDefinition(metadata); } function encodeEnumType( node: EnumTypeDefinitionNode | EnumTypeExtensionNode, includeDirectives: boolean, ): EnumTypeDefinitionTuple { + const metadata = + includeDirectives && node.directives?.length + ? { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; + return createEnumTypeDefinition( (node.values ?? []).map((value) => value.name.value), - includeDirectives - ? node.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, + metadata, ); } @@ -187,14 +196,18 @@ function encodeObjectType( for (const field of node.fields ?? []) { fields[field.name.value] = encodeField(field, includeDirectives); } + const metadata = + includeDirectives && node.directives?.length + ? { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; return createObjectTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), - includeDirectives - ? node.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, + metadata, ); } @@ -226,14 +239,20 @@ function encodeInterfaceType( for (const field of node.fields ?? []) { fields[field.name.value] = encodeField(field, includeDirectives); } + + const metadata = + includeDirectives && node.directives?.length + ? { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; + return createInterfaceTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), - includeDirectives - ? node.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, + metadata, ); } @@ -241,13 +260,18 @@ function encodeUnionType( node: UnionTypeDefinitionNode | UnionTypeExtensionNode, includeDirectives: boolean, ): UnionTypeDefinitionTuple { + const metadata = + includeDirectives && node.directives?.length + ? { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; + return createUnionTypeDefinition( (node.types ?? []).map((type) => type.name.value), - includeDirectives - ? node.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, + metadata, ); } @@ -259,41 +283,46 @@ function encodeInputObjectType( for (const field of node.fields ?? []) { fields[field.name.value] = encodeInputValue(field); } - return createInputObjectTypeDefinition( - fields, - includeDirectives - ? node.directives - ?.map(encodeDirectiveTuple) - .filter((directive) => !!directive) - : undefined, - ); + + const metadata = + includeDirectives && node.directives?.length + ? { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + } + : undefined; + + return createInputObjectTypeDefinition(fields, metadata); } function encodeField( node: FieldDefinitionNode, includeDirectives: boolean, ): TypeReference | FieldDefinitionTuple { - let directiveTuples: DirectiveTuple[] | undefined; + let fieldMetadata: TypeDefinitionMetadata | undefined; if (includeDirectives && node.directives?.length) { - directiveTuples = node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive); + fieldMetadata = { + directives: node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive), + }; } if (!node.arguments?.length) { - if (directiveTuples) { - return [typeReferenceFromNode(node.type), undefined, directiveTuples]; + if (fieldMetadata) { + return [typeReferenceFromNode(node.type), undefined, fieldMetadata]; } return typeReferenceFromNode(node.type); } - if (directiveTuples) { + if (fieldMetadata) { return [ typeReferenceFromNode(node.type), encodeArguments(node), - directiveTuples, + fieldMetadata, ]; } return [typeReferenceFromNode(node.type), encodeArguments(node)]; @@ -332,7 +361,7 @@ function encodeDirective( encodeDirectiveLocation(node.value as DirectiveLocationEnum), ), encodeArguments(node), - node.repeatable, + { isRepeatable: node.repeatable }, ]; } else { return [ diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index 29ffc62b6..e2db4c710 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -5,11 +5,11 @@ import { getDirectiveDefinitionArgs, getDirectiveName, getFieldArgs, - getFieldDirectives, + getFieldMetadata, getFields, getInputObjectFields, - getTypeDefinitionDirectiveIndex, - getTypeDefinitionDirectives, + getTypeDefinitionMetadataIndex, + getTypeDefinitionMetadata, InputValueDefinitionRecord, InterfaceTypeDefinitionTuple, isInputObjectTypeDefinition, @@ -22,6 +22,7 @@ import { setFieldDirectives, TypeDefinitionsRecord, TypeDefinitionTuple, + TypeDefinitionMetadata, } from "../schema/definition"; import { inspect } from "../jsutils/inspect"; @@ -51,32 +52,41 @@ export function mergeSchemaDefinitions( return accumulator; } -function mergeTypeDirectives( +function mergeTypeMetadata( target: TypeDefinitionTuple, source: TypeDefinitionTuple, ): void { - const targetDirectives: DirectiveTuple[] | undefined = - getTypeDefinitionDirectives(target); - const sourceDirectives: DirectiveTuple[] | undefined = - getTypeDefinitionDirectives(source); + const targetMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(target); + const sourceMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(source); - const directiveIndex = getTypeDefinitionDirectiveIndex(target); - if (!sourceDirectives || !directiveIndex) { + const metadataIndex = getTypeDefinitionMetadataIndex(target); + if (!sourceMetadata || !metadataIndex) { return; } - if (!targetDirectives) { - target[directiveIndex] = [...sourceDirectives]; + if (!targetMetadata) { + target[metadataIndex] ??= {}; + const targetMetadata: TypeDefinitionMetadata = target[ + metadataIndex + ] as TypeDefinitionMetadata; + if (sourceMetadata?.directives) { + targetMetadata.directives = [...sourceMetadata.directives]; + } + return; } - for (const sourceDirective of sourceDirectives) { - const directiveName = sourceDirective[0]; - const exists = targetDirectives.some( - (d: DirectiveTuple) => d[0] === directiveName, - ); - if (!exists) { - targetDirectives.push(sourceDirective); + if (sourceMetadata.directives && targetMetadata.directives) { + for (const sourceDirective of sourceMetadata.directives) { + const directiveName = sourceDirective[0]; + const exists = targetMetadata.directives.some( + (d: DirectiveTuple) => d[0] === directiveName, + ); + if (!exists) { + targetMetadata.directives.push(sourceDirective); + } } } } @@ -121,7 +131,7 @@ export function mergeTypes( continue; } - mergeTypeDirectives(targetDef, sourceDef); + mergeTypeMetadata(targetDef, sourceDef); if ( (isObjectTypeDefinition(targetDef) && @@ -171,11 +181,16 @@ function mergeFields( mergeInputValues(targetArgs, sourceArgs); } - const sourceDirectives = getFieldDirectives(sourceDef); + const sourceDirectives = getFieldMetadata(sourceDef); if (sourceDirectives) { - const targetDirectives = - getFieldDirectives(targetDef) ?? setFieldDirectives(targetDef, []); - mergeFieldDirectives(targetDirectives, sourceDirectives); + const targetMetadata = + getFieldMetadata(targetDef) ?? setFieldDirectives(targetDef, {}); + if (targetMetadata.directives && sourceDirectives.directives) { + mergeFieldDirectives( + targetMetadata.directives, + sourceDirectives.directives, + ); + } } } } From 68853ef059b2ff87a4b25b5fced61a3ac2bc1ad5 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:17:35 +0100 Subject: [PATCH 07/10] descriptions added --- .../supermassive/src/schema/definition.ts | 10 +- .../encodeASTSchema.test.ts.snap | 2 +- .../__tests__/decodeASTSchema.test.ts | 35 +++ .../__tests__/fixtures/descriptionsSDL.ts | 33 +++ .../src/utilities/decodeASTSchema.ts | 103 ++++++-- .../src/utilities/encodeASTSchema.ts | 228 ++++++++---------- 6 files changed, 256 insertions(+), 155 deletions(-) create mode 100644 packages/supermassive/src/utilities/__tests__/fixtures/descriptionsSDL.ts diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 0010a85ae..fddac16e2 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -25,14 +25,18 @@ const enum ScalarKeys { metadata = 1, } +export type Description = { + value: string; + block?: boolean; +}; export type TypeDefinitionMetadata = { directives?: DirectiveTuple[]; - description?: string; + description?: Description; }; export type DirectiveDefinitionMetadata = { - isRepeatable?: boolean; - description?: string; + repeatable?: boolean; + description?: Description; }; export type ObjectTypeDefinitionTuple = [ diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index dd82f07c4..112f9310c 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -47,7 +47,7 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "name": 6, }, { - "isRepeatable": true, + "repeatable": true, }, ], [ diff --git a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts index e2e0f4246..27572cf6e 100644 --- a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts @@ -5,6 +5,7 @@ import { kitchenSinkSDL } from "./fixtures/kitchenSinkSDL"; import { swapiSDL } from "./fixtures/swapiSDL"; import { mergeSchemaDefinitions } from "../mergeSchemaDefinitions"; import { schemaWithBooleanParameter } from "./fixtures/schemaWithBooleanParameter"; +import { descriptionsSDL } from "./fixtures/descriptionsSDL"; describe(decodeASTSchema, () => { test("correctly encodes swapi AST schema", () => { @@ -17,6 +18,40 @@ describe(decodeASTSchema, () => { expect(print(decoded)).toMatchSnapshot(); }); + test("correctly encodes description AST schema", () => { + const decoded = decodeASTSchema( + encodeASTSchema(descriptionsSDL.document, { + includeDescriptions: true, + includeDirectives: true, + }), + ); + expect(print(decoded)).toMatchInlineSnapshot(` + """"Type Description""" + type TypeWithDescription implements Node { + """Field Description""" + fieldWithDescription: Int + } + + """ + Input Description + second line + third line + """ + input AdvancedInputWithDescription { + enumField: NodeType! + } + + """Enum Description""" + enum EnumWithDescription { + VALUE_WITH_DESCRIPTION + } + + """Directive Description""" + directive @i18n(locale: String) on QUERY + " + `); + }); + test("correctly encodes kitchen sink AST schema", () => { const doc = cleanUpDocument(kitchenSinkSDL.document); const encoded = [ diff --git a/packages/supermassive/src/utilities/__tests__/fixtures/descriptionsSDL.ts b/packages/supermassive/src/utilities/__tests__/fixtures/descriptionsSDL.ts new file mode 100644 index 000000000..d45262fa4 --- /dev/null +++ b/packages/supermassive/src/utilities/__tests__/fixtures/descriptionsSDL.ts @@ -0,0 +1,33 @@ +import { gql } from "../../../__testUtils__/gql"; + +export const descriptionsSDL = gql` + """ + Type Description + """ + type TypeWithDescription implements Node { + """ + Field Description + """ + fieldWithDescription: Int + } + + """ + Input Description + second line + third line + """ + input AdvancedInputWithDescription { + enumField: NodeType! + } + + """ + Enum Description + """ + enum EnumWithDescription { + VALUE_WITH_DESCRIPTION + } + """ + Directive Description + """ + directive @i18n(locale: String) on QUERY +`; diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index d7cd00eef..e01580f27 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -58,6 +58,7 @@ import { DirectiveTuple, getDirectiveMetadata, getFieldMetadata, + Description, } from "../schema/definition"; import { inspectTypeReference, @@ -71,6 +72,7 @@ import { invariant } from "../jsutils/invariant"; import { ValueNode as ConstValueNode, DirectiveNode, + StringValueNode, } from "graphql/language/ast"; // TODO: use ConstValueNode in graphql@17 import { inspect } from "../jsutils/inspect"; @@ -124,15 +126,20 @@ function decodeScalarType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): ScalarTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getScalarTypeMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getScalarTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.SCALAR_TYPE_DEFINITION, name: nameNode(typeName), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -142,11 +149,15 @@ function decodeEnumType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getEnumMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getEnumMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.ENUM_TYPE_DEFINITION, name: nameNode(typeName), @@ -154,7 +165,8 @@ function decodeEnumType( kind: Kind.ENUM_VALUE_DEFINITION, name: nameNode(value), })), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -164,11 +176,15 @@ function decodeObjectType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getObjectTypeMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getObjectTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.OBJECT_TYPE_DEFINITION, name: nameNode(typeName), @@ -177,7 +193,8 @@ function decodeObjectType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -187,11 +204,15 @@ function decodeInterfaceType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getInterfaceTypeMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getInterfaceTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: nameNode(typeName), @@ -200,7 +221,8 @@ function decodeInterfaceType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -210,11 +232,15 @@ function decodeUnionType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getUnionTypeMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getUnionTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.UNION_TYPE_DEFINITION, name: nameNode(typeName), @@ -222,7 +248,8 @@ function decodeUnionType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -232,18 +259,23 @@ function decodeInputObjectType( types: TypeDefinitionsRecord, directives?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { - const decoded = decodeDirectiveTuple( - getInputTypeMetadata(tuple)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getInputTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); + return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, name: nameNode(typeName), fields: Object.entries(getInputObjectFields(tuple)).map(([name, value]) => decodeInputValue(name, value, types), ), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -254,8 +286,11 @@ function decodeFields( ): FieldDefinitionNode[] { return Object.entries(fields).map(([name, value]) => { const type = decodeTypeReference(getFieldTypeReference(value)); - const decoded = decodeDirectiveTuple( - getFieldMetadata(value)?.directives, + const { directives: metadataDirectives, description: metadataDescription } = + getFieldMetadata(value) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirectiveTuple( + metadataDirectives, types, directives, ); @@ -264,7 +299,8 @@ function decodeFields( name: nameNode(name), type, arguments: decodeArguments(getFieldArgs(value) ?? {}, types), - ...(decoded && { directives: decoded }), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; }); } @@ -397,6 +433,7 @@ function decodeDirective( const name = getDirectiveName(tuple); const args = getDirectiveDefinitionArgs(tuple); const locations = getDirectiveLocations(tuple); + const { repeatable, description } = getDirectiveMetadata(tuple) || {}; return { kind: Kind.DIRECTIVE_DEFINITION, name: nameNode(name), @@ -405,7 +442,8 @@ function decodeDirective( kind: Kind.NAME, value: decodeDirectiveLocation(loc), })), - repeatable: Boolean(getDirectiveMetadata(tuple)?.isRepeatable), + description: decodeDescription(description), + repeatable: Boolean(repeatable), }; } @@ -429,14 +467,14 @@ function decodeDirectiveTuple( ); const argumentDefinitions = getDirectiveArguments(directiveTuple); - const isRepeatable = Boolean( - getDirectiveMetadata(directiveTuple)?.isRepeatable, + const repeatable = Boolean( + getDirectiveMetadata(directiveTuple)?.repeatable, ); return { kind: Kind.DIRECTIVE, name: nameNode(directiveName), - ...(isRepeatable && { isRepeatable }), + ...(repeatable && { repeatable }), arguments: args && argumentDefinitions ? Object.entries(args)?.map(([argName, argValue]) => { @@ -463,3 +501,18 @@ function decodeDirectiveTuple( }; }); } + +function decodeDescription( + description?: Description, +): StringValueNode | undefined { + if (!description) { + return; + } + + const { value, block } = description; + return { + kind: "StringValue", + value, + block, + }; +} diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index 6f744498f..773b1abae 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -40,92 +40,62 @@ import { encodeDirectiveLocation, DirectiveTuple, TypeDefinitionMetadata, + DirectiveDefinitionMetadata, } from "../schema/definition"; import { typeReferenceFromNode, TypeReference } from "../schema/reference"; import { valueFromASTUntyped } from "./valueFromASTUntyped"; export type EncodeASTSchemaOptions = { includeDirectives?: boolean; + includeDescriptions?: boolean; }; export function encodeASTSchema( schemaFragment: DocumentNode, options?: EncodeASTSchemaOptions, ): SchemaDefinitions[] { - const includeDirectives = Boolean(options?.includeDirectives); const fragments: SchemaDefinitions[] = [{ types: {} }]; const add = (name: string, def: TypeDefinitionTuple, extension = false) => addTypeDefinition(fragments, name, def, extension); for (const definition of schemaFragment.definitions) { if (definition.kind === "ObjectTypeDefinition") { - add( - definition.name.value, - encodeObjectType(definition, includeDirectives), - ); + add(definition.name.value, encodeObjectType(definition, options)); } else if (definition.kind === "InputObjectTypeDefinition") { - add( - definition.name.value, - encodeInputObjectType(definition, includeDirectives), - ); + add(definition.name.value, encodeInputObjectType(definition, options)); } else if (definition.kind === "EnumTypeDefinition") { - add(definition.name.value, encodeEnumType(definition, includeDirectives)); + add(definition.name.value, encodeEnumType(definition, options)); } else if (definition.kind === "UnionTypeDefinition") { - add( - definition.name.value, - encodeUnionType(definition, includeDirectives), - ); + add(definition.name.value, encodeUnionType(definition, options)); } else if (definition.kind === "InterfaceTypeDefinition") { - add( - definition.name.value, - encodeInterfaceType(definition, includeDirectives), - ); + add(definition.name.value, encodeInterfaceType(definition, options)); } else if (definition.kind === "ScalarTypeDefinition") { - add( - definition.name.value, - encodeScalarType(definition, includeDirectives), - ); + add(definition.name.value, encodeScalarType(definition, options)); } else if (definition.kind === "ObjectTypeExtension") { - add( - definition.name.value, - encodeObjectType(definition, includeDirectives), - true, - ); + add(definition.name.value, encodeObjectType(definition, options), true); } else if (definition.kind === "InputObjectTypeExtension") { add( definition.name.value, - encodeInputObjectType(definition, includeDirectives), + encodeInputObjectType(definition, options), true, ); } else if (definition.kind === "EnumTypeExtension") { - add( - definition.name.value, - encodeEnumType(definition, includeDirectives), - true, - ); + add(definition.name.value, encodeEnumType(definition, options), true); } else if (definition.kind === "UnionTypeExtension") { - add( - definition.name.value, - encodeUnionType(definition, includeDirectives), - true, - ); + add(definition.name.value, encodeUnionType(definition, options), true); } else if (definition.kind === "InterfaceTypeExtension") { add( definition.name.value, - encodeInterfaceType(definition, includeDirectives), + encodeInterfaceType(definition, options), true, ); } else if (definition.kind === "ScalarTypeExtension") { - add( - definition.name.value, - encodeScalarType(definition, includeDirectives), - true, - ); + add(definition.name.value, encodeScalarType(definition, options), true); } else if (definition.kind === "DirectiveDefinition") { if (!fragments[0].directives) { fragments[0].directives = []; } - fragments[0].directives.push(encodeDirective(definition)); + fragments[0].directives.push(encodeDirective(definition, options)); } } return fragments; @@ -155,59 +125,34 @@ function addTypeDefinition( function encodeScalarType( type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): ScalarTypeDefinitionTuple { - const metadata = - includeDirectives && type.directives - ? { - directives: type.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; - - return createScalarTypeDefinition(metadata); + return createScalarTypeDefinition(getTypeDefinitionMetadata(type, options)); } function encodeEnumType( node: EnumTypeDefinitionNode | EnumTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): EnumTypeDefinitionTuple { - const metadata = - includeDirectives && node.directives?.length - ? { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; - return createEnumTypeDefinition( (node.values ?? []).map((value) => value.name.value), - metadata, + getTypeDefinitionMetadata(node, options), ); } function encodeObjectType( node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): ObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field, includeDirectives); + fields[field.name.value] = encodeField(field, options); } - const metadata = - includeDirectives && node.directives?.length - ? { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; + return createObjectTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), - metadata, + getTypeDefinitionMetadata(node, options), ); } @@ -233,82 +178,51 @@ function encodeDirectiveTuple( function encodeInterfaceType( node: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): InterfaceTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field, includeDirectives); + fields[field.name.value] = encodeField(field, options); } - const metadata = - includeDirectives && node.directives?.length - ? { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; - return createInterfaceTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), - metadata, + getTypeDefinitionMetadata(node, options), ); } function encodeUnionType( node: UnionTypeDefinitionNode | UnionTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): UnionTypeDefinitionTuple { - const metadata = - includeDirectives && node.directives?.length - ? { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; - return createUnionTypeDefinition( (node.types ?? []).map((type) => type.name.value), - metadata, + getTypeDefinitionMetadata(node, options), ); } function encodeInputObjectType( node: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): InputObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { fields[field.name.value] = encodeInputValue(field); } - const metadata = - includeDirectives && node.directives?.length - ? { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - } - : undefined; - - return createInputObjectTypeDefinition(fields, metadata); + return createInputObjectTypeDefinition( + fields, + getTypeDefinitionMetadata(node, options), + ); } function encodeField( node: FieldDefinitionNode, - includeDirectives: boolean, + options?: EncodeASTSchemaOptions, ): TypeReference | FieldDefinitionTuple { - let fieldMetadata: TypeDefinitionMetadata | undefined; - - if (includeDirectives && node.directives?.length) { - fieldMetadata = { - directives: node.directives - .map(encodeDirectiveTuple) - .filter((directive) => !!directive), - }; - } + const fieldMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(node, options); if (!node.arguments?.length) { if (fieldMetadata) { @@ -352,16 +266,20 @@ function encodeInputValue( function encodeDirective( node: DirectiveDefinitionNode, + options?: EncodeASTSchemaOptions, ): DirectiveDefinitionTuple { + const directiveDefinitionMetadata: DirectiveDefinitionMetadata | undefined = + getDirectiveDefinitionMetadata(node, options); + if (node.arguments?.length) { - if (node.repeatable) { + if (directiveDefinitionMetadata) { return [ node.name.value, node.locations.map((node) => encodeDirectiveLocation(node.value as DirectiveLocationEnum), ), encodeArguments(node), - { isRepeatable: node.repeatable }, + directiveDefinitionMetadata, ]; } else { return [ @@ -373,14 +291,14 @@ function encodeDirective( ]; } } else { - if (node.repeatable) { + if (directiveDefinitionMetadata) { [ node.name.value, node.locations.map((node) => encodeDirectiveLocation(node.value as DirectiveLocationEnum), ), undefined, - node.repeatable || undefined, + directiveDefinitionMetadata, ]; } return [ @@ -391,3 +309,61 @@ function encodeDirective( ]; } } + +function getDirectiveDefinitionMetadata( + node: T & { + repeatable?: boolean; + description?: { value: string; block?: boolean }; + }, + options?: EncodeASTSchemaOptions, +) { + let metadata: undefined | DirectiveDefinitionMetadata; + const { includeDescriptions } = options || {}; + + if (includeDescriptions && node.description) { + metadata ??= {}; + metadata.description = { + block: node.description.block, + value: node.description.value, + }; + } + + if (node.repeatable) { + metadata ??= {}; + metadata.repeatable = node.repeatable; + } + + return metadata; +} + +function getTypeDefinitionMetadata( + node: T & { + directives?: readonly DirectiveNode[]; + description?: { value: string; block?: boolean }; + }, + options?: EncodeASTSchemaOptions, +) { + let metadata: undefined | TypeDefinitionMetadata; + const { includeDirectives, includeDescriptions } = options || {}; + + if (includeDirectives && node.directives?.length) { + const directives = node.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive); + + if (directives.length) { + metadata ??= {}; + metadata.directives = directives; + } + } + + if (includeDescriptions && node.description) { + metadata ??= {}; + metadata.description = { + block: node.description.block, + value: node.description.value, + }; + } + + return metadata; +} From 75a1ece27320f8cb71d48a5767a17befadbbb029 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:44:45 +0100 Subject: [PATCH 08/10] Cleaning directive vs directiveDefinition --- .../supermassive/src/schema/definition.ts | 34 +++--- .../supermassive/src/schema/directives.ts | 12 +- .../src/utilities/decodeASTSchema.ts | 109 ++++++++++-------- .../src/utilities/encodeASTSchema.ts | 3 - ...ctMinimalViableSchemaForRequestDocument.ts | 4 +- .../src/utilities/mergeSchemaDefinitions.ts | 17 +-- .../utilities/subtractSchemaDefinitions.ts | 10 +- packages/supermassive/src/values.ts | 8 +- 8 files changed, 112 insertions(+), 85 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index fddac16e2..c5e419df8 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -191,7 +191,13 @@ export type DirectiveDefinitionTuple = [ arguments?: InputValueDefinitionRecord, metadata?: DirectiveDefinitionMetadata, ]; + const enum DirectiveKeys { + name = 0, + arguments = 1, +} + +const enum DirectiveDefinitionKeys { name = 0, locations = 1, arguments = 2, @@ -483,14 +489,16 @@ export function isSubType( return false; } -export function getDirectiveName(tuple: DirectiveDefinitionTuple): string { - return tuple[DirectiveKeys.name]; +export function getDirectiveDefinitionName( + tuple: DirectiveDefinitionTuple, +): string { + return tuple[DirectiveDefinitionKeys.name]; } -export function getDirectiveLocations( +export function getDirectiveDefinitionLocations( tuple: DirectiveDefinitionTuple, ): DirectiveLocation[] { - return tuple[DirectiveKeys.locations]; + return tuple[DirectiveDefinitionKeys.locations]; } export function encodeDirectiveLocation( @@ -615,14 +623,16 @@ export function getEnumMetadata( export function getDirectiveDefinitionArgs( directive: DirectiveDefinitionTuple, ): InputValueDefinitionRecord | undefined { - return directive[DirectiveKeys.arguments]; + return Array.isArray(directive) + ? directive[DirectiveDefinitionKeys.arguments] + : undefined; } export function setDirectiveDefinitionArgs( directive: DirectiveDefinitionTuple, args: InputValueDefinitionRecord, ): InputValueDefinitionRecord { - directive[DirectiveKeys.arguments] = args; + directive[DirectiveDefinitionKeys.arguments] = args; return args; } @@ -755,14 +765,12 @@ export function getFieldArguments( return Array.isArray(def) ? def[FieldKeys.arguments] : undefined; } -export function getDirectiveArguments( +export function getDirectiveDefinitionMetadata( def: DirectiveDefinitionTuple, -): InputValueDefinitionRecord | undefined { - return Array.isArray(def) ? def[DirectiveKeys.arguments] : undefined; +): DirectiveDefinitionMetadata | undefined { + return Array.isArray(def) ? def[DirectiveDefinitionKeys.metadata] : undefined; } -export function getDirectiveMetadata( - def: DirectiveDefinitionTuple, -): DirectiveDefinitionMetadata | undefined { - return Array.isArray(def) ? def[DirectiveKeys.metadata] : undefined; +export function getDirectiveName(tuple: DirectiveTuple): string { + return tuple[DirectiveKeys.name]; } diff --git a/packages/supermassive/src/schema/directives.ts b/packages/supermassive/src/schema/directives.ts index 862003b11..dfbded40f 100644 --- a/packages/supermassive/src/schema/directives.ts +++ b/packages/supermassive/src/schema/directives.ts @@ -3,7 +3,7 @@ import { DirectiveDefinitionTuple, DirectiveName, encodeDirectiveLocation, - getDirectiveName, + getDirectiveDefinitionName, } from "./definition"; /** @@ -106,7 +106,9 @@ export function isKnownDirective( directive: DirectiveName | DirectiveDefinitionTuple, ): boolean { const name = - typeof directive === "string" ? directive : getDirectiveName(directive); + typeof directive === "string" + ? directive + : getDirectiveDefinitionName(directive); return ( isSpecifiedDirective(directive) || name === SUPERMASSIVE_SCHEMA_DIRECTIVE_NAME @@ -116,8 +118,10 @@ export function isSpecifiedDirective( directive: DirectiveName | DirectiveDefinitionTuple, ): boolean { const name = - typeof directive === "string" ? directive : getDirectiveName(directive); + typeof directive === "string" + ? directive + : getDirectiveDefinitionName(directive); return specifiedDirectives.some( - (specDirective) => getDirectiveName(specDirective) === name, + (specDirective) => getDirectiveDefinitionName(specDirective) === name, ); } diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index e01580f27..08ff76672 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -43,9 +43,9 @@ import { getFieldArgs, getInputValueTypeReference, getInputDefaultValue, - getDirectiveName, + getDirectiveDefinitionName, getDirectiveDefinitionArgs, - getDirectiveLocations, + getDirectiveDefinitionLocations, decodeDirectiveLocation, getObjectTypeMetadata, getInterfaceTypeMetadata, @@ -54,9 +54,8 @@ import { ScalarTypeDefinitionTuple, getScalarTypeMetadata, getInputTypeMetadata, - getDirectiveArguments, DirectiveTuple, - getDirectiveMetadata, + getDirectiveDefinitionMetadata, getFieldMetadata, Description, } from "../schema/definition"; @@ -88,29 +87,39 @@ export function decodeASTSchema( } const definitions = []; const types = encodedSchemaFragments[0].types; - const directives = encodedSchemaFragments[0].directives; + const directiveDefinitions = encodedSchemaFragments[0].directives; for (const typeName in types) { const tuple = types[typeName]; if (isScalarTypeDefinition(tuple)) { - definitions.push(decodeScalarType(typeName, tuple, types, directives)); + definitions.push( + decodeScalarType(typeName, tuple, types, directiveDefinitions), + ); } else if (isEnumTypeDefinition(tuple)) { - definitions.push(decodeEnumType(typeName, tuple, types, directives)); + definitions.push( + decodeEnumType(typeName, tuple, types, directiveDefinitions), + ); } else if (isObjectTypeDefinition(tuple)) { - definitions.push(decodeObjectType(typeName, tuple, types, directives)); + definitions.push( + decodeObjectType(typeName, tuple, types, directiveDefinitions), + ); } else if (isInterfaceTypeDefinition(tuple)) { - definitions.push(decodeInterfaceType(typeName, tuple, types, directives)); + definitions.push( + decodeInterfaceType(typeName, tuple, types, directiveDefinitions), + ); } else if (isUnionTypeDefinition(tuple)) { - definitions.push(decodeUnionType(typeName, tuple, types, directives)); + definitions.push( + decodeUnionType(typeName, tuple, types, directiveDefinitions), + ); } else if (isInputObjectTypeDefinition(tuple)) { definitions.push( - decodeInputObjectType(typeName, tuple, types, directives), + decodeInputObjectType(typeName, tuple, types, directiveDefinitions), ); } } - for (const directive of directives ?? []) { - definitions.push(decodeDirective(directive, types)); + for (const directiveDefinition of directiveDefinitions ?? []) { + definitions.push(decodeDirectiveDefinition(directiveDefinition, types)); } return { kind: Kind.DOCUMENT, definitions }; @@ -124,15 +133,15 @@ function decodeScalarType( typeName: string, tuple: ScalarTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): ScalarTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getScalarTypeMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { @@ -147,15 +156,15 @@ function decodeEnumType( typeName: string, tuple: EnumTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getEnumMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { @@ -174,21 +183,25 @@ function decodeObjectType( typeName: string, tuple: ObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getObjectTypeMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { kind: Kind.OBJECT_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getObjectFields(tuple) ?? {}, types, directives), + fields: decodeFields( + getObjectFields(tuple) ?? {}, + types, + directiveDefinitions, + ), interfaces: getObjectTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), @@ -202,21 +215,21 @@ function decodeInterfaceType( typeName: string, tuple: InterfaceTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getInterfaceTypeMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getFields(tuple), types, directives), + fields: decodeFields(getFields(tuple), types, directiveDefinitions), interfaces: getInterfaceTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), @@ -230,15 +243,15 @@ function decodeUnionType( typeName: string, tuple: UnionTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getUnionTypeMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { @@ -257,15 +270,15 @@ function decodeInputObjectType( typeName: string, tuple: InputObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { const { directives: metadataDirectives, description: metadataDescription } = getInputTypeMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { @@ -282,17 +295,17 @@ function decodeInputObjectType( function decodeFields( fields: Record, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): FieldDefinitionNode[] { return Object.entries(fields).map(([name, value]) => { const type = decodeTypeReference(getFieldTypeReference(value)); const { directives: metadataDirectives, description: metadataDescription } = getFieldMetadata(value) || {}; const decodedDescription = decodeDescription(metadataDescription); - const decodedDirectives = decodeDirectiveTuple( + const decodedDirectives = decodeDirective( metadataDirectives, types, - directives, + directiveDefinitions, ); return { kind: Kind.FIELD_DEFINITION, @@ -426,14 +439,15 @@ function decodeTypeReference( }; } -function decodeDirective( +function decodeDirectiveDefinition( tuple: DirectiveDefinitionTuple, types: TypeDefinitionsRecord, ): DirectiveDefinitionNode { - const name = getDirectiveName(tuple); + const name = getDirectiveDefinitionName(tuple); const args = getDirectiveDefinitionArgs(tuple); - const locations = getDirectiveLocations(tuple); - const { repeatable, description } = getDirectiveMetadata(tuple) || {}; + const locations = getDirectiveDefinitionLocations(tuple); + const { repeatable, description } = + getDirectiveDefinitionMetadata(tuple) || {}; return { kind: Kind.DIRECTIVE_DEFINITION, name: nameNode(name), @@ -447,18 +461,19 @@ function decodeDirective( }; } -function decodeDirectiveTuple( +function decodeDirective( directiveTuples: DirectiveTuple[] | undefined, types: TypeDefinitionsRecord, - directives?: DirectiveDefinitionTuple[], + directiveDefinitions?: DirectiveDefinitionTuple[], ): ReadonlyArray | undefined { - if (!directiveTuples || !directives) { + if (!directiveTuples || !directiveDefinitions) { return; } return directiveTuples.map(([directiveName, args]) => { - const directiveTuple = directives?.find( - (directive) => getDirectiveName(directive) === directiveName, + const directiveTuple = directiveDefinitions?.find( + (directiveDefinition) => + getDirectiveDefinitionName(directiveDefinition) === directiveName, ); invariant( @@ -466,9 +481,9 @@ function decodeDirectiveTuple( `Could not find directive definition for "${directiveName}"`, ); - const argumentDefinitions = getDirectiveArguments(directiveTuple); + const argumentDefinitions = getDirectiveDefinitionArgs(directiveTuple); const repeatable = Boolean( - getDirectiveMetadata(directiveTuple)?.repeatable, + getDirectiveDefinitionMetadata(directiveTuple)?.repeatable, ); return { diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index 773b1abae..bb5dcbcaa 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -148,7 +148,6 @@ function encodeObjectType( for (const field of node.fields ?? []) { fields[field.name.value] = encodeField(field, options); } - return createObjectTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), @@ -164,7 +163,6 @@ function encodeDirectiveTuple( } const name = directive.name.value; - const args = Object.create(null); for (const argument of directive.arguments ?? []) { args[argument.name.value] = valueFromASTUntyped(argument.value); @@ -184,7 +182,6 @@ function encodeInterfaceType( for (const field of node.fields ?? []) { fields[field.name.value] = encodeField(field, options); } - return createInterfaceTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), diff --git a/packages/supermassive/src/utilities/extractMinimalViableSchemaForRequestDocument.ts b/packages/supermassive/src/utilities/extractMinimalViableSchemaForRequestDocument.ts index ef8d25f9b..a8851111a 100644 --- a/packages/supermassive/src/utilities/extractMinimalViableSchemaForRequestDocument.ts +++ b/packages/supermassive/src/utilities/extractMinimalViableSchemaForRequestDocument.ts @@ -39,7 +39,7 @@ import { encodeDirectiveLocation, EnumTypeDefinitionTuple, FieldDefinition, - getDirectiveName, + getDirectiveDefinitionName, getFieldArgs, getFields, getFieldTypeReference, @@ -278,7 +278,7 @@ function addDirective( directive: GraphQLDirective, ) { const name = directive.name; - let tuple = directives.find((d) => getDirectiveName(d) === name); + let tuple = directives.find((d) => getDirectiveDefinitionName(d) === name); if (!tuple) { tuple = [directive.name, directive.locations.map(encodeDirectiveLocation)]; directives.push(tuple); diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index e2db4c710..b5960ea29 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -3,7 +3,7 @@ import { DirectiveTuple, FieldDefinitionRecord, getDirectiveDefinitionArgs, - getDirectiveName, + getDirectiveDefinitionName, getFieldArgs, getFieldMetadata, getFields, @@ -23,6 +23,7 @@ import { TypeDefinitionsRecord, TypeDefinitionTuple, TypeDefinitionMetadata, + getDirectiveName, } from "../schema/definition"; import { inspect } from "../jsutils/inspect"; @@ -74,15 +75,14 @@ function mergeTypeMetadata( if (sourceMetadata?.directives) { targetMetadata.directives = [...sourceMetadata.directives]; } - return; } if (sourceMetadata.directives && targetMetadata.directives) { for (const sourceDirective of sourceMetadata.directives) { - const directiveName = sourceDirective[0]; + const directiveName = getDirectiveName(sourceDirective); const exists = targetMetadata.directives.some( - (d: DirectiveTuple) => d[0] === directiveName, + (d: DirectiveTuple) => getDirectiveName(d) === directiveName, ); if (!exists) { targetMetadata.directives.push(sourceDirective); @@ -98,7 +98,8 @@ export function mergeDirectives( for (const sourceDirective of source) { const targetDirective = target.find( (directive) => - getDirectiveName(directive) === getDirectiveName(sourceDirective), + getDirectiveDefinitionName(directive) === + getDirectiveDefinitionName(sourceDirective), ); if (!targetDirective) { target.push(sourceDirective); @@ -200,8 +201,10 @@ function mergeFieldDirectives( source: DirectiveTuple[], ): void { for (const sourceDirective of source) { - const directiveName = sourceDirective[0]; - const exists = target.some((d: DirectiveTuple) => d[0] === directiveName); + const directiveName = getDirectiveName(sourceDirective); + const exists = target.some( + (d: DirectiveTuple) => getDirectiveName(d) === directiveName, + ); if (!exists) { target.push(sourceDirective); } diff --git a/packages/supermassive/src/utilities/subtractSchemaDefinitions.ts b/packages/supermassive/src/utilities/subtractSchemaDefinitions.ts index 05dd32b36..7bfa80d98 100644 --- a/packages/supermassive/src/utilities/subtractSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/subtractSchemaDefinitions.ts @@ -7,7 +7,7 @@ import { TypeDefinitionsRecord, TypeDefinitionTuple, getDirectiveDefinitionArgs, - getDirectiveName, + getDirectiveDefinitionName, getEnumValues, getFieldTypeReference, getFieldArgs, @@ -461,9 +461,9 @@ function subtractDirectives( const result: DirectiveDefinitionTuple[] = []; for (const minuendDirective of minuendDirectives) { - const minuendName = getDirectiveName(minuendDirective); + const minuendName = getDirectiveDefinitionName(minuendDirective); const subtrahendDirective = subtrahendDirectives.find( - (d) => getDirectiveName(d) === minuendName, + (d) => getDirectiveDefinitionName(d) === minuendName, ); if (!subtrahendDirective) { @@ -519,9 +519,9 @@ function subtractDirectives( // Check for directives in subtrahend that don't exist in minuend for (const subtrahendDirective of subtrahendDirectives) { - const subtrahendName = getDirectiveName(subtrahendDirective); + const subtrahendName = getDirectiveDefinitionName(subtrahendDirective); const exists = minuendDirectives.some( - (d) => getDirectiveName(d) === subtrahendName, + (d) => getDirectiveDefinitionName(d) === subtrahendName, ); if (!exists) { if (strict) { diff --git a/packages/supermassive/src/values.ts b/packages/supermassive/src/values.ts index 8c8b00ccd..72ea4b1f8 100644 --- a/packages/supermassive/src/values.ts +++ b/packages/supermassive/src/values.ts @@ -14,12 +14,12 @@ import { DirectiveDefinitionTuple, FieldDefinition, getFieldArguments, - getDirectiveName, + getDirectiveDefinitionName, getInputDefaultValue, getInputValueTypeReference, isDefined, isInputType, - getDirectiveArguments, + getDirectiveDefinitionArgs, } from "./schema/definition"; import { valueFromAST } from "./utilities/valueFromAST"; import { coerceInputValue } from "./utilities/coerceInputValue"; @@ -172,7 +172,7 @@ export function getArgumentValues( const argumentDefs = node.kind === Kind.FIELD ? getFieldArguments(def as FieldDefinition) - : getDirectiveArguments(def as DirectiveDefinitionTuple); + : getDirectiveDefinitionArgs(def as DirectiveDefinitionTuple); if (!argumentDefs) { return coercedValues; } @@ -282,7 +282,7 @@ export function getDirectiveValues( directiveDef: DirectiveDefinitionTuple, node: { directives?: ReadonlyArray }, ): undefined | { [argument: string]: unknown } { - const name = getDirectiveName(directiveDef); + const name = getDirectiveDefinitionName(directiveDef); // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const directiveNode = node.directives?.find( From cb9c7ef6c33047e424df60694f693a8e147765be Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:14:53 +0100 Subject: [PATCH 09/10] basic tests added --- .../encodeASTSchema.test.ts.snap | 1176 +++++++++++++++++ .../__tests__/decodeASTSchema.test.ts | 28 + .../__tests__/encodeASTSchema.test.ts | 95 ++ 3 files changed, 1299 insertions(+) diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index 112f9310c..e61d7a7d4 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -414,6 +414,1182 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` ] `; +exports[`encodeASTSchema correctly encodes schema with both directives and descriptions 1`] = ` +[ + { + "directives": [ + [ + "skip", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + { + "description": { + "block": true, + "value": "This is a description of the \`@skip\` directive", + }, + }, + ], + [ + "include", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "include2", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "myRepeatableDir", + [ + 11, + 14, + ], + { + "name": 6, + }, + { + "repeatable": true, + }, + ], + [ + "onType", + [ + 11, + ], + ], + [ + "onObject", + [ + 11, + ], + { + "arg": 6, + }, + ], + [ + "onUnion", + [ + 15, + ], + ], + [ + "onField", + [ + 4, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "oneOf", + [ + 18, + ], + ], + [ + "onInputObject", + [ + 18, + ], + ], + ], + "types": { + "AnnotatedEnum": [ + 5, + [ + "ANNOTATED_VALUE", + "OTHER_VALUE", + ], + { + "directives": [ + [ + "onEnum", + ], + ], + }, + ], + "AnnotatedInput": [ + 6, + { + "annotatedField": "Type", + }, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, + ], + "AnnotatedInterface": [ + 3, + { + "annotatedField": [ + "Type", + { + "arg": "Type", + }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onInterface", + ], + ], + }, + ], + "AnnotatedObject": [ + 2, + { + "annotatedField": [ + "Type", + { + "arg": [ + "Type", + "default", + ], + }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onObject", + { + "arg": "value", + }, + ], + ], + }, + ], + "AnnotatedScalar": [ + 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, + ], + "AnnotatedUnion": [ + 4, + [ + "A", + "B", + ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "AnnotatedUnionTwo": [ + 4, + [ + "A", + "B", + ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "Bar": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + }, + ], + "Baz": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Bar", + "Two", + ], + ], + "CustomScalar": [ + 1, + ], + "Feed": [ + 4, + [ + "Story", + "Article", + "Advert", + ], + ], + "Foo": [ + 2, + { + "eight": [ + "Type", + { + "argument": "OneOfInputType", + }, + ], + "five": [ + 1, + { + "argument": [ + 11, + [ + "string", + "string", + ], + ], + }, + ], + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": [ + "Type", + undefined, + { + "description": { + "block": false, + "value": "Description of the \`one\` field.", + }, + }, + ], + "seven": [ + "Type", + { + "argument": [ + 3, + null, + ], + }, + ], + "six": [ + "Type", + { + "argument": [ + "InputType", + { + "key": "value", + }, + ], + }, + ], + "three": [ + 3, + { + "argument": "InputType", + "other": 1, + }, + { + "description": { + "block": true, + "value": "This is a description of the \`three\` field.", + }, + }, + ], + "two": [ + "Type", + { + "argument": "InputType!", + }, + { + "description": { + "block": true, + "value": "This is a description of the \`two\` field.", + }, + }, + ], + }, + [ + "Bar", + "Baz", + "Two", + ], + { + "description": { + "block": true, + "value": "This is a description +of the \`Foo\` type.", + }, + }, + ], + "InputType": [ + 6, + { + "answer": [ + 3, + 42, + ], + "key": 6, + }, + ], + "OneOfInputType": [ + 6, + { + "int": 3, + "string": 1, + }, + { + "directives": [ + [ + "oneOf", + ], + ], + }, + ], + "Site": [ + 5, + [ + "DESKTOP", + "MOBILE", + "WEB", + ], + ], + "UndefinedEnum": [ + 5, + [], + ], + "UndefinedInput": [ + 6, + {}, + ], + "UndefinedInterface": [ + 3, + {}, + ], + "UndefinedType": [ + 2, + {}, + ], + "UndefinedUnion": [ + 4, + [], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + { + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Two", + ], + ], + "CustomScalar": [ + 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, + ], + "Feed": [ + 4, + [ + "Photo", + "Video", + ], + ], + "Foo": [ + 2, + { + "seven": [ + "Type", + { + "argument": 11, + }, + ], + }, + ], + "InputType": [ + 6, + { + "other": [ + 4, + 12300, + ], + }, + ], + "Site": [ + 5, + [ + "VR", + ], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + {}, + [], + { + "directives": [ + [ + "onInterface", + ], + ], + }, + ], + "Feed": [ + 4, + [], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "Foo": [ + 2, + {}, + [], + { + "directives": [ + [ + "onType", + ], + ], + }, + ], + "InputType": [ + 6, + {}, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, + ], + "Site": [ + 5, + [], + { + "directives": [ + [ + "onEnum", + ], + ], + }, + ], + }, + }, +] +`; + +exports[`encodeASTSchema correctly encodes schema with descriptions when includeDescriptions is true 1`] = ` +[ + { + "directives": [ + [ + "i18n", + [ + 1, + ], + { + "locale": 1, + }, + { + "description": { + "block": true, + "value": "Directive Description", + }, + }, + ], + ], + "types": { + "AdvancedInputWithDescription": [ + 6, + { + "enumField": "NodeType!", + }, + { + "description": { + "block": true, + "value": "Input Description +second line +third line", + }, + }, + ], + "EnumWithDescription": [ + 5, + [ + "VALUE_WITH_DESCRIPTION", + ], + { + "description": { + "block": true, + "value": "Enum Description", + }, + }, + ], + "TypeWithDescription": [ + 2, + { + "fieldWithDescription": [ + 3, + undefined, + { + "description": { + "block": true, + "value": "Field Description", + }, + }, + ], + }, + [ + "Node", + ], + { + "description": { + "block": true, + "value": "Type Description", + }, + }, + ], + }, + }, +] +`; + +exports[`encodeASTSchema correctly encodes schema with directives when includeDirectives is true 1`] = ` +[ + { + "directives": [ + [ + "skip", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "include", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "include2", + [ + 4, + 6, + 7, + ], + { + "if": 7, + }, + ], + [ + "myRepeatableDir", + [ + 11, + 14, + ], + { + "name": 6, + }, + { + "repeatable": true, + }, + ], + [ + "onType", + [ + 11, + ], + ], + [ + "onObject", + [ + 11, + ], + { + "arg": 6, + }, + ], + [ + "onUnion", + [ + 15, + ], + ], + [ + "onField", + [ + 4, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "oneOf", + [ + 18, + ], + ], + [ + "onInputObject", + [ + 18, + ], + ], + ], + "types": { + "AnnotatedEnum": [ + 5, + [ + "ANNOTATED_VALUE", + "OTHER_VALUE", + ], + { + "directives": [ + [ + "onEnum", + ], + ], + }, + ], + "AnnotatedInput": [ + 6, + { + "annotatedField": "Type", + }, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, + ], + "AnnotatedInterface": [ + 3, + { + "annotatedField": [ + "Type", + { + "arg": "Type", + }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onInterface", + ], + ], + }, + ], + "AnnotatedObject": [ + 2, + { + "annotatedField": [ + "Type", + { + "arg": [ + "Type", + "default", + ], + }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onObject", + { + "arg": "value", + }, + ], + ], + }, + ], + "AnnotatedScalar": [ + 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, + ], + "AnnotatedUnion": [ + 4, + [ + "A", + "B", + ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "AnnotatedUnionTwo": [ + 4, + [ + "A", + "B", + ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "Bar": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + }, + ], + "Baz": [ + 3, + { + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Bar", + "Two", + ], + ], + "CustomScalar": [ + 1, + ], + "Feed": [ + 4, + [ + "Story", + "Article", + "Advert", + ], + ], + "Foo": [ + 2, + { + "eight": [ + "Type", + { + "argument": "OneOfInputType", + }, + ], + "five": [ + 1, + { + "argument": [ + 11, + [ + "string", + "string", + ], + ], + }, + ], + "four": [ + 1, + { + "argument": [ + 1, + "string", + ], + }, + ], + "one": "Type", + "seven": [ + "Type", + { + "argument": [ + 3, + null, + ], + }, + ], + "six": [ + "Type", + { + "argument": [ + "InputType", + { + "key": "value", + }, + ], + }, + ], + "three": [ + 3, + { + "argument": "InputType", + "other": 1, + }, + ], + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Bar", + "Baz", + "Two", + ], + ], + "InputType": [ + 6, + { + "answer": [ + 3, + 42, + ], + "key": 6, + }, + ], + "OneOfInputType": [ + 6, + { + "int": 3, + "string": 1, + }, + { + "directives": [ + [ + "oneOf", + ], + ], + }, + ], + "Site": [ + 5, + [ + "DESKTOP", + "MOBILE", + "WEB", + ], + ], + "UndefinedEnum": [ + 5, + [], + ], + "UndefinedInput": [ + 6, + {}, + ], + "UndefinedInterface": [ + 3, + {}, + ], + "UndefinedType": [ + 2, + {}, + ], + "UndefinedUnion": [ + 4, + [], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + { + "two": [ + "Type", + { + "argument": "InputType!", + }, + ], + }, + [ + "Two", + ], + ], + "CustomScalar": [ + 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, + ], + "Feed": [ + 4, + [ + "Photo", + "Video", + ], + ], + "Foo": [ + 2, + { + "seven": [ + "Type", + { + "argument": 11, + }, + ], + }, + ], + "InputType": [ + 6, + { + "other": [ + 4, + 12300, + ], + }, + ], + "Site": [ + 5, + [ + "VR", + ], + ], + }, + }, + { + "types": { + "Bar": [ + 3, + {}, + [], + { + "directives": [ + [ + "onInterface", + ], + ], + }, + ], + "Feed": [ + 4, + [], + { + "directives": [ + [ + "onUnion", + ], + ], + }, + ], + "Foo": [ + 2, + {}, + [], + { + "directives": [ + [ + "onType", + ], + ], + }, + ], + "InputType": [ + 6, + {}, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, + ], + "Site": [ + 5, + [], + { + "directives": [ + [ + "onEnum", + ], + ], + }, + ], + }, + }, +] +`; + exports[`encodeASTSchema correctly encodes swapi AST schema 1`] = ` [ { diff --git a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts index 27572cf6e..2394dc9ea 100644 --- a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts @@ -101,6 +101,34 @@ describe(decodeASTSchema, () => { expect(encodeASTSchema(decoded)).toEqual(encoded); expect(print(decoded)).toMatchSnapshot(); }); + + test("correctly handles schema directives", () => { + const doc = cleanUpDocument(kitchenSinkSDL.document); + const encoded = [ + mergeSchemaDefinitions( + { types: {}, directives: [] }, + encodeASTSchema(doc, { includeDirectives: true }), + ), + ]; + const decoded = decodeASTSchema(encoded); + + const reEncoded = encodeASTSchema(decoded, { includeDirectives: true }); + expect(reEncoded).toEqual(encoded); + }); + + test("correctly handles schema descriptions", () => { + const doc = cleanUpDocument(kitchenSinkSDL.document); + const encoded = [ + mergeSchemaDefinitions( + { types: {}, directives: [] }, + encodeASTSchema(doc, { includeDescriptions: true }), + ), + ]; + const decoded = decodeASTSchema(encoded); + + const reEncoded = encodeASTSchema(decoded, { includeDescriptions: true }); + expect(reEncoded).toEqual(encoded); + }); }); function cleanUpDocument(doc: DocumentNode): DocumentNode { diff --git a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts index d4e31b3d1..642e1ad76 100644 --- a/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts +++ b/packages/supermassive/src/utilities/__tests__/encodeASTSchema.test.ts @@ -1,6 +1,7 @@ import { encodeASTSchema } from "../encodeASTSchema"; import { swapiSDL } from "./fixtures/swapiSDL"; import { kitchenSinkSDL } from "./fixtures/kitchenSinkSDL"; +import { descriptionsSDL } from "./fixtures/descriptionsSDL"; describe(encodeASTSchema, () => { test("correctly encodes swapi AST schema", () => { @@ -12,4 +13,98 @@ describe(encodeASTSchema, () => { const encoded = encodeASTSchema(kitchenSinkSDL.document); expect(encoded).toMatchSnapshot(); }); + + test("correctly encodes schema with directives when includeDirectives is true", () => { + const encoded = encodeASTSchema(kitchenSinkSDL.document, { + includeDirectives: true, + }); + + const schemaDefinitions = encoded[0]; + + expect(schemaDefinitions.directives).toBeDefined(); + expect(schemaDefinitions.directives?.length).toBeGreaterThan(0); + + expect(encoded).toMatchSnapshot(); + }); + + test("correctly encodes schema with descriptions when includeDescriptions is true", () => { + const encoded = encodeASTSchema(descriptionsSDL.document, { + includeDescriptions: true, + }); + expect(encoded).toMatchSnapshot(); + + const schemaDefinitions = encoded[0]; + const typeWithDescription = schemaDefinitions.types["TypeWithDescription"]; + expect(typeWithDescription).toMatchInlineSnapshot(` + [ + 2, + { + "fieldWithDescription": [ + 3, + undefined, + { + "description": { + "block": true, + "value": "Field Description", + }, + }, + ], + }, + [ + "Node", + ], + { + "description": { + "block": true, + "value": "Type Description", + }, + }, + ] + `); + + const inputWithDescription = + schemaDefinitions.types["AdvancedInputWithDescription"]; + + expect(inputWithDescription).toMatchInlineSnapshot(` + [ + 6, + { + "enumField": "NodeType!", + }, + { + "description": { + "block": true, + "value": "Input Description + second line + third line", + }, + }, + ] + `); + + const enumWithDescription = schemaDefinitions.types["EnumWithDescription"]; + expect(enumWithDescription).toMatchInlineSnapshot(` + [ + 5, + [ + "VALUE_WITH_DESCRIPTION", + ], + { + "description": { + "block": true, + "value": "Enum Description", + }, + }, + ] + `); + }); + + test("correctly encodes schema with both directives and descriptions", () => { + const encoded = encodeASTSchema(kitchenSinkSDL.document, { + includeDirectives: true, + includeDescriptions: true, + }); + + expect(encoded).toMatchSnapshot(); + }); }); From b84ac8e9f3fbe542d82fab6a54b5080b13b98d9e Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:47:17 +0100 Subject: [PATCH 10/10] enum values descriptions and directives added --- .../supermassive/src/schema/definition.ts | 12 ++-- .../decodeASTSchema.test.ts.snap | 6 +- .../encodeASTSchema.test.ts.snap | 58 +++++++++++++++++++ .../__tests__/fixtures/kitchenSinkSDL.ts | 2 + .../src/utilities/decodeASTSchema.ts | 31 ++++++++-- .../src/utilities/encodeASTSchema.ts | 43 +++++++++++++- 6 files changed, 139 insertions(+), 13 deletions(-) diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index c5e419df8..a16eb9ad0 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -29,9 +29,13 @@ export type Description = { value: string; block?: boolean; }; -export type TypeDefinitionMetadata = { +export interface TypeDefinitionMetadata { directives?: DirectiveTuple[]; description?: Description; +} + +export type EnumTypeDefinitionMetadata = TypeDefinitionMetadata & { + values?: Record; }; export type DirectiveDefinitionMetadata = { @@ -76,7 +80,7 @@ const enum UnionKeys { export type EnumTypeDefinitionTuple = [ kind: TypeKind.ENUM, values: string[], - metadata?: TypeDefinitionMetadata, + metadata?: EnumTypeDefinitionMetadata, ]; const enum EnumKeys { values = 1, @@ -616,7 +620,7 @@ export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { export function getEnumMetadata( tuple: EnumTypeDefinitionTuple, -): TypeDefinitionMetadata | undefined { +): EnumTypeDefinitionMetadata | undefined { return tuple[EnumKeys.metadata]; } @@ -692,7 +696,7 @@ export function createInputObjectTypeDefinition( export function createEnumTypeDefinition( values: string[], - metadata?: TypeDefinitionMetadata, + metadata?: EnumTypeDefinitionMetadata, ): EnumTypeDefinitionTuple { if (metadata) { return [TypeKind.ENUM, values, metadata]; diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index 55800d5fc..62b12b38c 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap @@ -106,6 +106,8 @@ directive @onUnion on UNION directive @onField on FIELD +directive @onEnumValue on ENUM_VALUE + directive @onInterface on INTERFACE directive @onScalar on SCALAR @@ -173,7 +175,7 @@ enum Site @onEnum { } enum AnnotatedEnum @onEnum { - ANNOTATED_VALUE + ANNOTATED_VALUE @onEnumValue OTHER_VALUE } @@ -212,6 +214,8 @@ directive @onUnion on UNION directive @onField on FIELD +directive @onEnumValue on ENUM_VALUE + directive @onInterface on INTERFACE directive @onScalar on SCALAR diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap index e61d7a7d4..41a345bf6 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -77,6 +77,12 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` 4, ], ], + [ + "onEnumValue", + [ + 17, + ], + ], [ "onInterface", [ @@ -497,6 +503,12 @@ exports[`encodeASTSchema correctly encodes schema with both directives and descr 4, ], ], + [ + "onEnumValue", + [ + 17, + ], + ], [ "onInterface", [ @@ -541,6 +553,15 @@ exports[`encodeASTSchema correctly encodes schema with both directives and descr "onEnum", ], ], + "values": { + "ANNOTATED_VALUE": { + "directives": [ + [ + "onEnumValue", + ], + ], + }, + }, }, ], "AnnotatedInput": [ @@ -833,6 +854,28 @@ of the \`Foo\` type.", "MOBILE", "WEB", ], + { + "values": { + "DESKTOP": { + "description": { + "block": true, + "value": "This is a description of the \`DESKTOP\` value", + }, + }, + "MOBILE": { + "description": { + "block": true, + "value": "This is a description of the \`MOBILE\` value", + }, + }, + "WEB": { + "description": { + "block": false, + "value": "This is a description of the \`WEB\` value", + }, + }, + }, + }, ], "UndefinedEnum": [ 5, @@ -1134,6 +1177,12 @@ exports[`encodeASTSchema correctly encodes schema with directives when includeDi 4, ], ], + [ + "onEnumValue", + [ + 17, + ], + ], [ "onInterface", [ @@ -1178,6 +1227,15 @@ exports[`encodeASTSchema correctly encodes schema with directives when includeDi "onEnum", ], ], + "values": { + "ANNOTATED_VALUE": { + "directives": [ + [ + "onEnumValue", + ], + ], + }, + }, }, ], "AnnotatedInput": [ diff --git a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts index c309aeaab..c33e23f3f 100644 --- a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts +++ b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts @@ -163,6 +163,8 @@ export const kitchenSinkSDL = gql` directive @onField on FIELD + directive @onEnumValue on ENUM_VALUE + directive @onInterface on INTERFACE directive @onScalar on SCALAR diff --git a/packages/supermassive/src/utilities/decodeASTSchema.ts b/packages/supermassive/src/utilities/decodeASTSchema.ts index 08ff76672..06e3ca88b 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -158,8 +158,11 @@ function decodeEnumType( types: TypeDefinitionsRecord, directiveDefinitions?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { - const { directives: metadataDirectives, description: metadataDescription } = - getEnumMetadata(tuple) || {}; + const { + directives: metadataDirectives, + description: metadataDescription, + values, + } = getEnumMetadata(tuple) || {}; const decodedDescription = decodeDescription(metadataDescription); const decodedDirectives = decodeDirective( metadataDirectives, @@ -170,10 +173,26 @@ function decodeEnumType( return { kind: Kind.ENUM_TYPE_DEFINITION, name: nameNode(typeName), - values: getEnumValues(tuple).map((value) => ({ - kind: Kind.ENUM_VALUE_DEFINITION, - name: nameNode(value), - })), + values: getEnumValues(tuple).map((value) => { + const valueMetadata = values?.[value]; + const decodedValueDescription = decodeDescription( + valueMetadata?.description, + ); + const decodedValueDirectives = decodeDirective( + valueMetadata?.directives, + types, + directiveDefinitions, + ); + + return { + kind: Kind.ENUM_VALUE_DEFINITION, + name: nameNode(value), + ...(decodedValueDirectives && { directives: decodedValueDirectives }), + ...(decodedValueDescription && { + description: decodedValueDescription, + }), + }; + }), ...(decodedDirectives && { directives: decodedDirectives }), ...(decodedDescription && { description: decodedDescription }), }; diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index bb5dcbcaa..108c66652 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -41,6 +41,7 @@ import { DirectiveTuple, TypeDefinitionMetadata, DirectiveDefinitionMetadata, + EnumTypeDefinitionMetadata, } from "../schema/definition"; import { typeReferenceFromNode, TypeReference } from "../schema/reference"; import { valueFromASTUntyped } from "./valueFromASTUntyped"; @@ -136,7 +137,7 @@ function encodeEnumType( ): EnumTypeDefinitionTuple { return createEnumTypeDefinition( (node.values ?? []).map((value) => value.name.value), - getTypeDefinitionMetadata(node, options), + getEnumTypeDefinitionMetadata(node, options), ); } @@ -333,13 +334,51 @@ function getDirectiveDefinitionMetadata( return metadata; } +function getEnumTypeDefinitionMetadata( + node: EnumTypeDefinitionNode | EnumTypeExtensionNode, + options?: EncodeASTSchemaOptions, +): EnumTypeDefinitionMetadata | undefined { + const { includeDirectives, includeDescriptions } = options || {}; + let valuesMetadadata: Record | undefined; + + if (includeDirectives || includeDescriptions) { + for (const value of node?.values || []) { + if (value.directives?.length || value.description) { + if (includeDirectives && value.directives?.length) { + valuesMetadadata ??= {}; + valuesMetadadata[value.name.value] ??= {}; + valuesMetadadata[value.name.value]["directives"] = value.directives + .map(encodeDirectiveTuple) + .filter((directive) => !!directive); + } + + if (includeDescriptions && value.description) { + valuesMetadadata ??= {}; + valuesMetadadata[value.name.value] ??= {}; + valuesMetadadata[value.name.value]["description"] = { + block: value.description.block, + value: value.description.value, + }; + } + } + } + } + const enumTypeMetadata = getTypeDefinitionMetadata(node, options); + if (enumTypeMetadata || valuesMetadadata) { + return { + ...getTypeDefinitionMetadata(node, options), + ...(valuesMetadadata && { values: valuesMetadadata }), + }; + } +} + function getTypeDefinitionMetadata( node: T & { directives?: readonly DirectiveNode[]; description?: { value: string; block?: boolean }; }, options?: EncodeASTSchemaOptions, -) { +): TypeDefinitionMetadata | undefined { let metadata: undefined | TypeDefinitionMetadata; const { includeDirectives, includeDescriptions } = options || {};