diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 9de6f5796..a16eb9ad0 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -18,56 +18,83 @@ const enum TypeKind { export type ScalarTypeDefinitionTuple = [ kind: TypeKind.SCALAR, - // directives?: DirectiveTuple[], // TODO ? + metadata?: TypeDefinitionMetadata, ]; +const enum ScalarKeys { + metadata = 1, +} + +export type Description = { + value: string; + block?: boolean; +}; +export interface TypeDefinitionMetadata { + directives?: DirectiveTuple[]; + description?: Description; +} + +export type EnumTypeDefinitionMetadata = TypeDefinitionMetadata & { + values?: Record; +}; + +export type DirectiveDefinitionMetadata = { + repeatable?: boolean; + description?: Description; +}; + export type ObjectTypeDefinitionTuple = [ kind: TypeKind.OBJECT, fields: FieldDefinitionRecord, interfaces?: TypeName[], - // directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum ObjectKeys { fields = 1, interfaces = 2, + metadata = 3, } export type InterfaceTypeDefinitionTuple = [ kind: TypeKind.INTERFACE, fields: FieldDefinitionRecord, interfaces?: TypeName[], - // directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum InterfaceKeys { fields = 1, interfaces = 2, + metadata = 3, } export type UnionTypeDefinitionTuple = [ kind: TypeKind.UNION, types: TypeName[], - // directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum UnionKeys { types = 1, + metadata = 2, } export type EnumTypeDefinitionTuple = [ kind: TypeKind.ENUM, values: string[], - // directives?: DirectiveTuple[], + metadata?: EnumTypeDefinitionMetadata, ]; const enum EnumKeys { values = 1, + metadata = 2, } export type InputObjectTypeDefinitionTuple = [ kind: TypeKind.INPUT, fields: InputValueDefinitionRecord, - // directives?: DirectiveTuple[], + metadata?: TypeDefinitionMetadata, ]; const enum InputObjectKeys { fields = 1, + metadata = 2, } export type TypeDefinitionTuple = @@ -85,12 +112,14 @@ export type CompositeTypeTuple = export type FieldDefinitionTuple = [ type: TypeReference, - arguments: InputValueDefinitionRecord, - // directives?: DirectiveTuple[], + // TODO should I really do it ? + arguments?: InputValueDefinitionRecord, + metadata?: TypeDefinitionMetadata, ]; const enum FieldKeys { type = 0, arguments = 1, + metadata = 2, } export type FieldDefinition = TypeReference | FieldDefinitionTuple; export type FieldDefinitionRecord = Record; @@ -98,7 +127,7 @@ export type FieldDefinitionRecord = Record; export type InputValueDefinitionTuple = [ type: TypeReference, defaultValue: unknown, - // directives?: DirectiveTuple[], + metadata?: DirectiveDefinitionMetadata, ]; const enum InputValueKeys { type = 0, @@ -155,19 +184,28 @@ 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) + arguments?: Record, // JS values (cannot be a variable inside schema definition, so it is fine) ]; export type DirectiveDefinitionTuple = [ name: DirectiveName, locations: DirectiveLocation[], arguments?: InputValueDefinitionRecord, + metadata?: DirectiveDefinitionMetadata, ]; + const enum DirectiveKeys { + name = 0, + arguments = 1, +} + +const enum DirectiveDefinitionKeys { name = 0, locations = 1, arguments = 2, + metadata = 3, } export type TypeDefinitionsRecord = Record; @@ -188,6 +226,40 @@ export type SchemaDefinitions = { const typeNameMetaFieldDef: FieldDefinition = "String"; const specifiedScalarDefinition: ScalarTypeDefinitionTuple = [TypeKind.SCALAR]; +export function getTypeDefinitionMetadataIndex( + typeDefinition: TypeDefinitionTuple, +): number | undefined { + if (isObjectTypeDefinition(typeDefinition)) { + return ObjectKeys.metadata; + } else if (isScalarTypeDefinition(typeDefinition)) { + return ScalarKeys.metadata; + } else if (isEnumTypeDefinition(typeDefinition)) { + return EnumKeys.metadata; + } else if (isInterfaceTypeDefinition(typeDefinition)) { + return InterfaceKeys.metadata; + } else if (isInputObjectTypeDefinition(typeDefinition)) { + return InputObjectKeys.metadata; + } else if (isUnionTypeDefinition(typeDefinition)) { + return UnionKeys.metadata; + } +} + +export function getTypeDefinitionMetadata(typeDefinition: TypeDefinitionTuple) { + if (isObjectTypeDefinition(typeDefinition)) { + return getObjectTypeMetadata(typeDefinition); + } else if (isScalarTypeDefinition(typeDefinition)) { + return getScalarTypeMetadata(typeDefinition); + } else if (isEnumTypeDefinition(typeDefinition)) { + return getEnumMetadata(typeDefinition); + } else if (isInterfaceTypeDefinition(typeDefinition)) { + return getInterfaceTypeMetadata(typeDefinition); + } else if (isInputObjectTypeDefinition(typeDefinition)) { + return getInputTypeMetadata(typeDefinition); + } else if (isUnionTypeDefinition(typeDefinition)) { + return getUnionTypeMetadata(typeDefinition); + } +} + export function findObjectType( defs: SchemaDefinitions, typeName: TypeName, @@ -421,14 +493,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( @@ -518,6 +592,12 @@ export function getFieldArgs( return Array.isArray(field) ? field[FieldKeys.arguments] : undefined; } +export function getFieldMetadata( + field: FieldDefinition, +): TypeDefinitionMetadata | undefined { + return Array.isArray(field) ? field[FieldKeys.metadata] : undefined; +} + export function setFieldArgs( field: FieldDefinitionTuple, args: InputValueDefinitionRecord, @@ -526,64 +606,127 @@ export function setFieldArgs( return args; } +export function setFieldDirectives( + field: FieldDefinitionTuple, + args: TypeDefinitionMetadata, +): TypeDefinitionMetadata { + field[FieldKeys.metadata] = args; + return args; +} + export function getEnumValues(tuple: EnumTypeDefinitionTuple): string[] { return tuple[EnumKeys.values]; } +export function getEnumMetadata( + tuple: EnumTypeDefinitionTuple, +): EnumTypeDefinitionMetadata | undefined { + return tuple[EnumKeys.metadata]; +} + 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; } export function createUnionTypeDefinition( types: TypeName[], + metadata?: TypeDefinitionMetadata, ): UnionTypeDefinitionTuple { + if (metadata) { + return [TypeKind.UNION, types, metadata]; + } + return [TypeKind.UNION, types]; } export function createInterfaceTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], + metadata?: TypeDefinitionMetadata, ): InterfaceTypeDefinitionTuple { - return interfaces?.length - ? [TypeKind.INTERFACE, fields, interfaces] - : [TypeKind.INTERFACE, fields]; + if (!interfaces?.length && !metadata) { + return [TypeKind.INTERFACE, fields]; + } + + if (interfaces?.length && !metadata) { + return [TypeKind.INTERFACE, fields, interfaces]; + } + + return [TypeKind.INTERFACE, fields, interfaces, metadata]; } export function createObjectTypeDefinition( fields: FieldDefinitionRecord, interfaces?: TypeName[], + metadata?: TypeDefinitionMetadata, ): ObjectTypeDefinitionTuple { - return interfaces?.length - ? [TypeKind.OBJECT, fields, interfaces] - : [TypeKind.OBJECT, fields]; + if (!interfaces?.length && !metadata) { + return [TypeKind.OBJECT, fields]; + } + + if (interfaces?.length && !metadata) { + return [TypeKind.OBJECT, fields, interfaces]; + } + + return [TypeKind.OBJECT, fields, interfaces, metadata]; } export function createInputObjectTypeDefinition( fields: InputValueDefinitionRecord, + metadata?: TypeDefinitionMetadata, ): InputObjectTypeDefinitionTuple { + if (metadata) { + return [TypeKind.INPUT, fields, metadata]; + } + return [TypeKind.INPUT, fields]; } export function createEnumTypeDefinition( values: string[], + metadata?: EnumTypeDefinitionMetadata, ): EnumTypeDefinitionTuple { + if (metadata) { + return [TypeKind.ENUM, values, metadata]; + } + return [TypeKind.ENUM, values]; } -export function createScalarTypeDefinition(): ScalarTypeDefinitionTuple { +export function createScalarTypeDefinition( + metadata?: TypeDefinitionMetadata, +): ScalarTypeDefinitionTuple { + if (metadata) { + return [TypeKind.SCALAR, metadata]; + } + return [TypeKind.SCALAR]; } +export function getScalarTypeMetadata( + def: ScalarTypeDefinitionTuple, +): TypeDefinitionMetadata | undefined { + return def[ScalarKeys.metadata]; +} + +export function getObjectTypeMetadata( + def: ObjectTypeDefinitionTuple, +): TypeDefinitionMetadata | undefined { + return def[ObjectKeys.metadata]; +} + export function getObjectTypeInterfaces( def: ObjectTypeDefinitionTuple, ): TypeName[] { @@ -596,20 +739,42 @@ export function getInterfaceTypeInterfaces( return def[InterfaceKeys.interfaces] ?? []; } +export function getInterfaceTypeMetadata( + def: InterfaceTypeDefinitionTuple, +): TypeDefinitionMetadata | undefined { + return def[InterfaceKeys.metadata]; +} + +export function getInputTypeMetadata( + def: InputObjectTypeDefinitionTuple, +): TypeDefinitionMetadata | undefined { + return def[InputObjectKeys.metadata]; +} + export function getUnionTypeMembers( tuple: UnionTypeDefinitionTuple, ): TypeName[] { return tuple[UnionKeys.types]; } +export function getUnionTypeMetadata( + def: UnionTypeDefinitionTuple, +): TypeDefinitionMetadata | undefined { + return def[UnionKeys.metadata]; +} + export function getFieldArguments( def: FieldDefinition, ): InputValueDefinitionRecord | undefined { 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 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/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index d69d6c6fd..62b12b38c 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,135 @@ 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 + +directive @onObject(arg: String!) on OBJECT + +directive @onUnion on UNION + +directive @onField on FIELD + +directive @onEnumValue on ENUM_VALUE + +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 @onEnumValue + 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 @onEnumValue on ENUM_VALUE + +directive @onInterface on INTERFACE + +directive @onScalar on SCALAR + +directive @onEnum on ENUM + +directive @oneOf on INPUT_OBJECT + +directive @onInputObject on INPUT_OBJECT " `; @@ -285,3 +413,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 ca0fc0c96..41a345bf6 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/encodeASTSchema.test.ts.snap @@ -46,6 +46,1172 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` { "name": 6, }, + { + "repeatable": true, + }, + ], + [ + "onType", + [ + 11, + ], + ], + [ + "onObject", + [ + 11, + ], + { + "arg": 6, + }, + ], + [ + "onUnion", + [ + 15, + ], + ], + [ + "onField", + [ + 4, + ], + ], + [ + "onEnumValue", + [ + 17, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "oneOf", + [ + 18, + ], + ], + [ + "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 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, + ], + ], + [ + "onEnumValue", + [ + 17, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "oneOf", + [ + 18, + ], + ], + [ + "onInputObject", + [ + 18, + ], + ], + ], + "types": { + "AnnotatedEnum": [ + 5, + [ + "ANNOTATED_VALUE", + "OTHER_VALUE", + ], + { + "directives": [ + [ + "onEnum", + ], + ], + "values": { + "ANNOTATED_VALUE": { + "directives": [ + [ + "onEnumValue", + ], + ], + }, + }, + }, + ], + "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", + ], + { + "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, + [], + ], + "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, + ], + ], + [ + "onEnumValue", + [ + 17, + ], + ], + [ + "onInterface", + [ + 14, + ], + ], + [ + "onScalar", + [ + 10, + ], + ], + [ + "onEnum", + [ + 16, + ], + ], + [ + "oneOf", + [ + 18, + ], + ], + [ + "onInputObject", + [ + 18, + ], ], ], "types": { @@ -55,12 +1221,35 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "ANNOTATED_VALUE", "OTHER_VALUE", ], + { + "directives": [ + [ + "onEnum", + ], + ], + "values": { + "ANNOTATED_VALUE": { + "directives": [ + [ + "onEnumValue", + ], + ], + }, + }, + }, ], "AnnotatedInput": [ 6, { "annotatedField": "Type", }, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, ], "AnnotatedInterface": [ 3, @@ -70,6 +1259,21 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` { "arg": "Type", }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onInterface", + ], ], }, ], @@ -84,11 +1288,36 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "default", ], }, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + }, + [], + { + "directives": [ + [ + "onObject", + { + "arg": "value", + }, + ], ], }, ], "AnnotatedScalar": [ 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, ], "AnnotatedUnion": [ 4, @@ -96,6 +1325,13 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "A", "B", ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, ], "AnnotatedUnionTwo": [ 4, @@ -103,6 +1339,13 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "A", "B", ], + { + "directives": [ + [ + "onUnion", + ], + ], + }, ], "Bar": [ 3, @@ -242,6 +1485,13 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "int": 3, "string": 1, }, + { + "directives": [ + [ + "oneOf", + ], + ], + }, ], "Site": [ 5, @@ -291,6 +1541,13 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` ], "CustomScalar": [ 1, + { + "directives": [ + [ + "onScalar", + ], + ], + }, ], "Feed": [ 4, @@ -332,22 +1589,59 @@ exports[`encodeASTSchema correctly encodes kitchen sink AST schema 1`] = ` "Bar": [ 3, {}, + [], + { + "directives": [ + [ + "onInterface", + ], + ], + }, ], "Feed": [ 4, [], + { + "directives": [ + [ + "onUnion", + ], + ], + }, ], "Foo": [ 2, {}, + [], + { + "directives": [ + [ + "onType", + ], + ], + }, ], "InputType": [ 6, {}, + { + "directives": [ + [ + "onInputObject", + ], + ], + }, ], "Site": [ 5, [], + { + "directives": [ + [ + "onEnum", + ], + ], + }, ], }, }, diff --git a/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts b/packages/supermassive/src/utilities/__tests__/decodeASTSchema.test.ts index 9a59fbaa5..2394dc9ea 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 = [ @@ -30,6 +65,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); @@ -39,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(); + }); }); 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/__tests__/fixtures/kitchenSinkSDL.ts b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts index 6ee03283d..c33e23f3f 100644 --- a/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts +++ b/packages/supermassive/src/utilities/__tests__/fixtures/kitchenSinkSDL.ts @@ -155,6 +155,26 @@ 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 @onField on FIELD + + directive @onEnumValue on ENUM_VALUE + + 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 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..cc0a93fa1 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", () => { @@ -245,7 +251,7 @@ describe("mergeSchemaDefinitions", () => { } extend type User implements Named { - name: String + name: String @testDirective } extend type User implements Contactable { @@ -322,6 +328,167 @@ 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, + { + "directives": [ + [ + "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, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + "name": 6, + }, + [], + { + "directives": [ + [ + "onExtendInterface", + ], + [ + "onInterface", + ], + ], + }, + ], + "Query": [ + 2, + { + "user": [ + 1, + { + "id": 6, + }, + { + "directives": [ + [ + "oneOf", + ], + [ + "context", + ], + ], + }, + ], + }, + ], + "User": [ + 2, + { + "id": [ + 10, + undefined, + { + "directives": [ + [ + "onField", + ], + ], + }, + ], + "name": 6, + }, + [ + "IUser", + ], + { + "directives": [ + [ + "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 f18bc3029..06e3ca88b 100644 --- a/packages/supermassive/src/utilities/decodeASTSchema.ts +++ b/packages/supermassive/src/utilities/decodeASTSchema.ts @@ -43,10 +43,21 @@ import { getFieldArgs, getInputValueTypeReference, getInputDefaultValue, - getDirectiveName, + getDirectiveDefinitionName, getDirectiveDefinitionArgs, - getDirectiveLocations, + getDirectiveDefinitionLocations, decodeDirectiveLocation, + getObjectTypeMetadata, + getInterfaceTypeMetadata, + getEnumMetadata, + getUnionTypeMetadata, + ScalarTypeDefinitionTuple, + getScalarTypeMetadata, + getInputTypeMetadata, + DirectiveTuple, + getDirectiveDefinitionMetadata, + getFieldMetadata, + Description, } from "../schema/definition"; import { inspectTypeReference, @@ -57,7 +68,11 @@ 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, + StringValueNode, +} from "graphql/language/ast"; // TODO: use ConstValueNode in graphql@17 import { inspect } from "../jsutils/inspect"; /** @@ -72,27 +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)); + definitions.push( + decodeScalarType(typeName, tuple, types, directiveDefinitions), + ); } else if (isEnumTypeDefinition(tuple)) { - definitions.push(decodeEnumType(typeName, tuple)); + definitions.push( + decodeEnumType(typeName, tuple, types, directiveDefinitions), + ); } else if (isObjectTypeDefinition(tuple)) { - definitions.push(decodeObjectType(typeName, tuple, types)); + definitions.push( + decodeObjectType(typeName, tuple, types, directiveDefinitions), + ); } else if (isInterfaceTypeDefinition(tuple)) { - definitions.push(decodeInterfaceType(typeName, tuple, types)); + definitions.push( + decodeInterfaceType(typeName, tuple, types, directiveDefinitions), + ); } else if (isUnionTypeDefinition(tuple)) { - definitions.push(decodeUnionType(typeName, tuple)); + definitions.push( + decodeUnionType(typeName, tuple, types, directiveDefinitions), + ); } else if (isInputObjectTypeDefinition(tuple)) { - definitions.push(decodeInputObjectType(typeName, tuple, types)); + definitions.push( + 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 }; @@ -102,24 +129,72 @@ function nameNode(value: string): NameNode { return { kind: Kind.NAME, value }; } -function decodeScalarType(typeName: string): ScalarTypeDefinitionNode { +function decodeScalarType( + typeName: string, + tuple: ScalarTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], +): ScalarTypeDefinitionNode { + const { directives: metadataDirectives, description: metadataDescription } = + getScalarTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + return { kind: Kind.SCALAR_TYPE_DEFINITION, name: nameNode(typeName), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } function decodeEnumType( typeName: string, tuple: EnumTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], ): EnumTypeDefinitionNode { + const { + directives: metadataDirectives, + description: metadataDescription, + values, + } = getEnumMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + 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 }), }; } @@ -127,15 +202,31 @@ function decodeObjectType( typeName: string, tuple: ObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], ): ObjectTypeDefinitionNode { + const { directives: metadataDirectives, description: metadataDescription } = + getObjectTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + return { kind: Kind.OBJECT_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getObjectFields(tuple) ?? {}, types), + fields: decodeFields( + getObjectFields(tuple) ?? {}, + types, + directiveDefinitions, + ), interfaces: getObjectTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), })), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -143,22 +234,45 @@ function decodeInterfaceType( typeName: string, tuple: InterfaceTypeDefinitionTuple, types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], ): InterfaceTypeDefinitionNode { + const { directives: metadataDirectives, description: metadataDescription } = + getInterfaceTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + return { kind: Kind.INTERFACE_TYPE_DEFINITION, name: nameNode(typeName), - fields: decodeFields(getFields(tuple), types), + fields: decodeFields(getFields(tuple), types, directiveDefinitions), interfaces: getInterfaceTypeInterfaces(tuple).map((name) => ({ kind: Kind.NAMED_TYPE, name: nameNode(name), })), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } function decodeUnionType( typeName: string, tuple: UnionTypeDefinitionTuple, + types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], ): UnionTypeDefinitionNode { + const { directives: metadataDirectives, description: metadataDescription } = + getUnionTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + return { kind: Kind.UNION_TYPE_DEFINITION, name: nameNode(typeName), @@ -166,6 +280,8 @@ function decodeUnionType( kind: Kind.NAMED_TYPE, name: nameNode(name), })), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } @@ -173,27 +289,50 @@ function decodeInputObjectType( typeName: string, tuple: InputObjectTypeDefinitionTuple, types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], ): InputObjectTypeDefinitionNode { + const { directives: metadataDirectives, description: metadataDescription } = + getInputTypeMetadata(tuple) || {}; + const decodedDescription = decodeDescription(metadataDescription); + const decodedDirectives = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); + return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, name: nameNode(typeName), fields: Object.entries(getInputObjectFields(tuple)).map(([name, value]) => decodeInputValue(name, value, types), ), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; } function decodeFields( fields: Record, types: TypeDefinitionsRecord, + 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 = decodeDirective( + metadataDirectives, + types, + directiveDefinitions, + ); return { kind: Kind.FIELD_DEFINITION, name: nameNode(name), type, arguments: decodeArguments(getFieldArgs(value) ?? {}, types), + ...(decodedDirectives && { directives: decodedDirectives }), + ...(decodedDescription && { description: decodedDescription }), }; }); } @@ -319,13 +458,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 locations = getDirectiveDefinitionLocations(tuple); + const { repeatable, description } = + getDirectiveDefinitionMetadata(tuple) || {}; return { kind: Kind.DIRECTIVE_DEFINITION, name: nameNode(name), @@ -334,7 +475,78 @@ function decodeDirective( kind: Kind.NAME, value: decodeDirectiveLocation(loc), })), - // TODO? repeatable are irrelevant for execution - repeatable: false, + description: decodeDescription(description), + repeatable: Boolean(repeatable), + }; +} + +function decodeDirective( + directiveTuples: DirectiveTuple[] | undefined, + types: TypeDefinitionsRecord, + directiveDefinitions?: DirectiveDefinitionTuple[], +): ReadonlyArray | undefined { + if (!directiveTuples || !directiveDefinitions) { + return; + } + + return directiveTuples.map(([directiveName, args]) => { + const directiveTuple = directiveDefinitions?.find( + (directiveDefinition) => + getDirectiveDefinitionName(directiveDefinition) === directiveName, + ); + + invariant( + directiveTuple !== undefined, + `Could not find directive definition for "${directiveName}"`, + ); + + const argumentDefinitions = getDirectiveDefinitionArgs(directiveTuple); + const repeatable = Boolean( + getDirectiveDefinitionMetadata(directiveTuple)?.repeatable, + ); + + return { + kind: Kind.DIRECTIVE, + name: nameNode(directiveName), + ...(repeatable && { repeatable }), + 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, + ), + }; + }) + : [], + }; + }); +} + +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 a8b1511a6..108c66652 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,12 +38,22 @@ import { createInterfaceTypeDefinition, createUnionTypeDefinition, encodeDirectiveLocation, + DirectiveTuple, + TypeDefinitionMetadata, + DirectiveDefinitionMetadata, + EnumTypeDefinitionMetadata, } 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 fragments: SchemaDefinitions[] = [{ types: {} }]; const add = (name: string, def: TypeDefinitionTuple, extension = false) => @@ -50,34 +61,42 @@ export function encodeASTSchema( for (const definition of schemaFragment.definitions) { if (definition.kind === "ObjectTypeDefinition") { - add(definition.name.value, encodeObjectType(definition)); + add(definition.name.value, encodeObjectType(definition, options)); } else if (definition.kind === "InputObjectTypeDefinition") { - add(definition.name.value, encodeInputObjectType(definition)); + add(definition.name.value, encodeInputObjectType(definition, options)); } else if (definition.kind === "EnumTypeDefinition") { - add(definition.name.value, encodeEnumType(definition)); + add(definition.name.value, encodeEnumType(definition, options)); } else if (definition.kind === "UnionTypeDefinition") { - add(definition.name.value, encodeUnionType(definition)); + add(definition.name.value, encodeUnionType(definition, options)); } else if (definition.kind === "InterfaceTypeDefinition") { - add(definition.name.value, encodeInterfaceType(definition)); + add(definition.name.value, encodeInterfaceType(definition, options)); } else if (definition.kind === "ScalarTypeDefinition") { - add(definition.name.value, encodeScalarType(definition)); + add(definition.name.value, encodeScalarType(definition, options)); } else if (definition.kind === "ObjectTypeExtension") { - add(definition.name.value, encodeObjectType(definition), true); + add(definition.name.value, encodeObjectType(definition, options), true); } else if (definition.kind === "InputObjectTypeExtension") { - add(definition.name.value, encodeInputObjectType(definition), true); + add( + definition.name.value, + encodeInputObjectType(definition, options), + true, + ); } else if (definition.kind === "EnumTypeExtension") { - add(definition.name.value, encodeEnumType(definition), true); + add(definition.name.value, encodeEnumType(definition, options), true); } else if (definition.kind === "UnionTypeExtension") { - add(definition.name.value, encodeUnionType(definition), true); + add(definition.name.value, encodeUnionType(definition, options), true); } else if (definition.kind === "InterfaceTypeExtension") { - add(definition.name.value, encodeInterfaceType(definition), true); + add( + definition.name.value, + encodeInterfaceType(definition, options), + true, + ); } else if (definition.kind === "ScalarTypeExtension") { - add(definition.name.value, encodeScalarType(definition), 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; @@ -106,69 +125,119 @@ function addTypeDefinition( } function encodeScalarType( - _type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, + type: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, + options?: EncodeASTSchemaOptions, ): ScalarTypeDefinitionTuple { - return createScalarTypeDefinition(); + return createScalarTypeDefinition(getTypeDefinitionMetadata(type, options)); } function encodeEnumType( node: EnumTypeDefinitionNode | EnumTypeExtensionNode, + options?: EncodeASTSchemaOptions, ): EnumTypeDefinitionTuple { return createEnumTypeDefinition( (node.values ?? []).map((value) => value.name.value), + getEnumTypeDefinitionMetadata(node, options), ); } function encodeObjectType( node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, + options?: EncodeASTSchemaOptions, ): ObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field); + fields[field.name.value] = encodeField(field, options); } return createObjectTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), + getTypeDefinitionMetadata(node, options), ); } +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, + options?: EncodeASTSchemaOptions, ): InterfaceTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { - fields[field.name.value] = encodeField(field); + fields[field.name.value] = encodeField(field, options); } return createInterfaceTypeDefinition( fields, node.interfaces?.map((iface) => iface.name.value), + getTypeDefinitionMetadata(node, options), ); } function encodeUnionType( node: UnionTypeDefinitionNode | UnionTypeExtensionNode, + options?: EncodeASTSchemaOptions, ): UnionTypeDefinitionTuple { return createUnionTypeDefinition( (node.types ?? []).map((type) => type.name.value), + getTypeDefinitionMetadata(node, options), ); } function encodeInputObjectType( node: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, + options?: EncodeASTSchemaOptions, ): InputObjectTypeDefinitionTuple { const fields = Object.create(null); for (const field of node.fields ?? []) { fields[field.name.value] = encodeInputValue(field); } - return createInputObjectTypeDefinition(fields); + + return createInputObjectTypeDefinition( + fields, + getTypeDefinitionMetadata(node, options), + ); } function encodeField( node: FieldDefinitionNode, + options?: EncodeASTSchemaOptions, ): TypeReference | FieldDefinitionTuple { - return !node.arguments?.length - ? typeReferenceFromNode(node.type) - : [typeReferenceFromNode(node.type), encodeArguments(node)]; + const fieldMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(node, options); + + if (!node.arguments?.length) { + if (fieldMetadata) { + return [typeReferenceFromNode(node.type), undefined, fieldMetadata]; + } + + return typeReferenceFromNode(node.type); + } + + if (fieldMetadata) { + return [ + typeReferenceFromNode(node.type), + encodeArguments(node), + fieldMetadata, + ]; + } + return [typeReferenceFromNode(node.type), encodeArguments(node)]; } function encodeArguments( @@ -195,16 +264,41 @@ function encodeInputValue( function encodeDirective( node: DirectiveDefinitionNode, + options?: EncodeASTSchemaOptions, ): DirectiveDefinitionTuple { + const directiveDefinitionMetadata: DirectiveDefinitionMetadata | undefined = + getDirectiveDefinitionMetadata(node, options); + if (node.arguments?.length) { - return [ - node.name.value, - node.locations.map((node) => - encodeDirectiveLocation(node.value as DirectiveLocationEnum), - ), - encodeArguments(node), - ]; + if (directiveDefinitionMetadata) { + return [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + encodeArguments(node), + directiveDefinitionMetadata, + ]; + } else { + return [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + encodeArguments(node), + ]; + } } else { + if (directiveDefinitionMetadata) { + [ + node.name.value, + node.locations.map((node) => + encodeDirectiveLocation(node.value as DirectiveLocationEnum), + ), + undefined, + directiveDefinitionMetadata, + ]; + } return [ node.name.value, node.locations.map((node) => @@ -213,3 +307,99 @@ 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 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 || {}; + + 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; +} 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 f7ab9040d..b5960ea29 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -1,11 +1,15 @@ import { DirectiveDefinitionTuple, + DirectiveTuple, FieldDefinitionRecord, getDirectiveDefinitionArgs, - getDirectiveName, + getDirectiveDefinitionName, getFieldArgs, + getFieldMetadata, getFields, getInputObjectFields, + getTypeDefinitionMetadataIndex, + getTypeDefinitionMetadata, InputValueDefinitionRecord, InterfaceTypeDefinitionTuple, isInputObjectTypeDefinition, @@ -15,7 +19,11 @@ import { SchemaDefinitions, setDirectiveDefinitionArgs, setFieldArgs, + setFieldDirectives, TypeDefinitionsRecord, + TypeDefinitionTuple, + TypeDefinitionMetadata, + getDirectiveName, } from "../schema/definition"; import { inspect } from "../jsutils/inspect"; @@ -45,6 +53,44 @@ export function mergeSchemaDefinitions( return accumulator; } +function mergeTypeMetadata( + target: TypeDefinitionTuple, + source: TypeDefinitionTuple, +): void { + const targetMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(target); + const sourceMetadata: TypeDefinitionMetadata | undefined = + getTypeDefinitionMetadata(source); + + const metadataIndex = getTypeDefinitionMetadataIndex(target); + if (!sourceMetadata || !metadataIndex) { + return; + } + + if (!targetMetadata) { + target[metadataIndex] ??= {}; + const targetMetadata: TypeDefinitionMetadata = target[ + metadataIndex + ] as TypeDefinitionMetadata; + if (sourceMetadata?.directives) { + targetMetadata.directives = [...sourceMetadata.directives]; + } + return; + } + + if (sourceMetadata.directives && targetMetadata.directives) { + for (const sourceDirective of sourceMetadata.directives) { + const directiveName = getDirectiveName(sourceDirective); + const exists = targetMetadata.directives.some( + (d: DirectiveTuple) => getDirectiveName(d) === directiveName, + ); + if (!exists) { + targetMetadata.directives.push(sourceDirective); + } + } + } +} + export function mergeDirectives( target: DirectiveDefinitionTuple[], source: DirectiveDefinitionTuple[], @@ -52,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); @@ -84,6 +131,9 @@ export function mergeTypes( target[typeName] = sourceDef; continue; } + + mergeTypeMetadata(targetDef, sourceDef); + if ( (isObjectTypeDefinition(targetDef) && isObjectTypeDefinition(sourceDef)) || @@ -131,6 +181,33 @@ function mergeFields( const targetArgs = getFieldArgs(targetDef) ?? setFieldArgs(targetDef, {}); mergeInputValues(targetArgs, sourceArgs); } + + const sourceDirectives = getFieldMetadata(sourceDef); + if (sourceDirectives) { + const targetMetadata = + getFieldMetadata(targetDef) ?? setFieldDirectives(targetDef, {}); + if (targetMetadata.directives && sourceDirectives.directives) { + mergeFieldDirectives( + targetMetadata.directives, + sourceDirectives.directives, + ); + } + } + } +} + +function mergeFieldDirectives( + target: DirectiveTuple[], + source: DirectiveTuple[], +): void { + for (const sourceDirective of source) { + 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(