From 902682e50159f54ae66853ff42af0b347d063c4d Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 29 Dec 2023 21:11:09 -0800 Subject: [PATCH 1/2] WIP scalar serialization --- src/codegen.ts | 147 ++++++++++++++++-- .../CustomScalarArgument.ts.expected | 19 ++- .../DefineCustomScalar.ts.expected | 19 ++- ...ineCustomScalarWithDescription.ts.expected | 19 ++- .../DefineRenamedCustomScalar.ts.expected | 19 ++- .../field_values/CustomScalar.ts.expected | 19 ++- 6 files changed, 216 insertions(+), 26 deletions(-) diff --git a/src/codegen.ts b/src/codegen.ts index 701b40e2..51b97146 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -35,6 +35,19 @@ import { } from "./metadataDirectives"; import { resolveRelativePath } from "./gratsRoot"; +const SCHEMA_CONFIG_TYPE_NAME = "SchemaConfigType"; +const SCHEMA_CONFIG_NAME = "config"; +const SCHEMA_CONFIG_SCALARS_NAME = "scalars"; + +const SCALAR_CONFIG_TYPE_NAME = "ScalarConfigType"; +const PRIMITIVE_TYPE_NAMES = new Set([ + "String", + "Int", + "Float", + "Boolean", + "ID", +]); + const F = ts.factory; // Given a GraphQL SDL, returns the a string of TypeScript code that generates a @@ -74,15 +87,33 @@ class Codegen { return F.createIdentifier(name); } - graphQLTypeImport(name: string): ts.TypeReferenceNode { + graphQLTypeImport( + name: string, + typeArguments?: readonly ts.TypeNode[], + ): ts.TypeReferenceNode { this._graphQLImports.add(name); - return F.createTypeReferenceNode(name); + return F.createTypeReferenceNode(name, typeArguments); } schemaDeclarationExport(): void { + const schemaConfigType = this.schemaConfigTypeDeclaration(); + const params: ts.ParameterDeclaration[] = []; + if (schemaConfigType != null) { + this._statements.push(schemaConfigType); + params.push( + F.createParameterDeclaration( + undefined, + undefined, + SCHEMA_CONFIG_NAME, + undefined, + F.createTypeReferenceNode(SCHEMA_CONFIG_TYPE_NAME), + ), + ); + } this.functionDeclaration( "getSchema", [F.createModifier(ts.SyntaxKind.ExportKeyword)], + params, this.graphQLTypeImport("GraphQLSchema"), this.createBlockWithScope(() => { this._statements.push( @@ -90,7 +121,7 @@ class Codegen { F.createNewExpression( this.graphQLImport("GraphQLSchema"), [], - [this.schemaConfig()], + [this.schemaConfigObject()], ), ), ); @@ -98,7 +129,90 @@ class Codegen { ); } - schemaConfig(): ts.ObjectLiteralExpression { + schemaConfigTypeDeclaration(): ts.TypeAliasDeclaration | null { + const configType = this.schemaConfigType(); + if (configType == null) return null; + return F.createTypeAliasDeclaration( + [F.createModifier(ts.SyntaxKind.ExportKeyword)], + SCHEMA_CONFIG_TYPE_NAME, + undefined, + configType, + ); + } + + schemaConfigType(): ts.TypeLiteralNode | null { + const scalarType = this.schemaConfigScalarType(); + if (scalarType == null) return null; + return F.createTypeLiteralNode([scalarType]); + } + + schemaConfigScalarType(): ts.TypeElement | null { + const typeMap = this._schema.getTypeMap(); + const scalarTypes = Object.values(typeMap) + .filter(isScalarType) + .filter((scalar) => { + // Built in primitives + return !PRIMITIVE_TYPE_NAMES.has(scalar.name); + }); + if (scalarTypes.length == 0) return null; + this._statements.push( + F.createTypeAliasDeclaration( + undefined, + SCALAR_CONFIG_TYPE_NAME, + [ + F.createTypeParameterDeclaration(undefined, "TInternal"), + F.createTypeParameterDeclaration(undefined, "TExternal"), + ], + F.createTypeLiteralNode([ + F.createPropertySignature( + undefined, + "serialize", + undefined, + this.graphQLTypeImport("GraphQLScalarSerializer", [ + F.createTypeReferenceNode("TExternal"), + ]), + ), + F.createPropertySignature( + undefined, + "parseValue", + undefined, + + this.graphQLTypeImport("GraphQLScalarValueParser", [ + F.createTypeReferenceNode("TInternal"), + ]), + ), + F.createPropertySignature( + undefined, + "parseLiteral", + undefined, + this.graphQLTypeImport("GraphQLScalarLiteralParser", [ + F.createTypeReferenceNode("TInternal"), + ]), + ), + ]), + ), + ); + return F.createPropertySignature( + undefined, + SCHEMA_CONFIG_SCALARS_NAME, + undefined, + F.createTypeLiteralNode( + scalarTypes.map((scalar) => { + return F.createPropertySignature( + undefined, + scalar.name, + undefined, + F.createTypeReferenceNode(SCALAR_CONFIG_TYPE_NAME, [ + F.createTypeReferenceNode("Date"), + F.createTypeReferenceNode("string"), + ]), + ); + }), + ), + ); + } + + schemaConfigObject(): ts.ObjectLiteralExpression { return this.objectLiteral([ this.description(this._schema.description), this.query(), @@ -115,12 +229,7 @@ class Codegen { type.name.startsWith("__") || type.name.startsWith("Introspection") || type.name.startsWith("Schema") || - // Built in primitives - type.name === "String" || - type.name === "Int" || - type.name === "Float" || - type.name === "Boolean" || - type.name === "ID" + PRIMITIVE_TYPE_NAMES.has(type.name) ); }) .map((type) => this.typeReference(type)); @@ -408,6 +517,21 @@ class Codegen { return this.objectLiteral([ this.description(obj.description), F.createPropertyAssignment("name", F.createStringLiteral(obj.name)), + ...["serialize", "parseValue", "parseLiteral"].map((name) => { + return F.createPropertyAssignment( + name, + F.createPropertyAccessExpression( + F.createPropertyAccessExpression( + F.createPropertyAccessExpression( + F.createIdentifier(SCHEMA_CONFIG_NAME), + SCHEMA_CONFIG_SCALARS_NAME, + ), + obj.name, + ), + name, + ), + ); + }), ]); } @@ -663,6 +787,7 @@ class Codegen { functionDeclaration( name: string, modifiers: ts.Modifier[] | undefined, + parameters: ts.ParameterDeclaration[], type: ts.TypeNode | undefined, body: ts.Block, ): void { @@ -672,7 +797,7 @@ class Codegen { undefined, name, undefined, - [], + parameters, type, body, ), diff --git a/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected b/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected index c3fac90f..9d990fd7 100644 --- a/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected +++ b/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected @@ -22,10 +22,23 @@ type SomeType { hello(greeting: MyString!): String } -- TypeScript -- -import { GraphQLSchema, GraphQLScalarType, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; -export function getSchema(): GraphQLSchema { +import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; +type ScalarConfigType = { + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + MyString: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { const MyStringType: GraphQLScalarType = new GraphQLScalarType({ - name: "MyString" + name: "MyString", + serialize: config.scalars.MyString.serialize, + parseValue: config.scalars.MyString.parseValue, + parseLiteral: config.scalars.MyString.parseLiteral }); const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({ name: "SomeType", diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected index 64098700..e9fda55b 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected @@ -20,8 +20,18 @@ type SomeType { scalar MyUrl -- TypeScript -- -import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -export function getSchema(): GraphQLSchema { +import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; +type ScalarConfigType = { + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + MyUrl: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({ name: "SomeType", fields() { @@ -34,7 +44,10 @@ export function getSchema(): GraphQLSchema { } }); const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ - name: "MyUrl" + name: "MyUrl", + serialize: config.scalars.MyUrl.serialize, + parseValue: config.scalars.MyUrl.parseValue, + parseLiteral: config.scalars.MyUrl.parseLiteral }); return new GraphQLSchema({ types: [SomeTypeType, MyUrlType] diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected index c8833f76..5fb84f90 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected @@ -24,8 +24,18 @@ type SomeType { """Use this for URLs.""" scalar MyUrl -- TypeScript -- -import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -export function getSchema(): GraphQLSchema { +import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; +type ScalarConfigType = { + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + MyUrl: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({ name: "SomeType", fields() { @@ -39,7 +49,10 @@ export function getSchema(): GraphQLSchema { }); const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ description: "Use this for URLs.", - name: "MyUrl" + name: "MyUrl", + serialize: config.scalars.MyUrl.serialize, + parseValue: config.scalars.MyUrl.parseValue, + parseLiteral: config.scalars.MyUrl.parseLiteral }); return new GraphQLSchema({ types: [SomeTypeType, MyUrlType] diff --git a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected index 9d27c358..a20734be 100644 --- a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected @@ -20,8 +20,18 @@ type SomeType { scalar CustomName -- TypeScript -- -import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -export function getSchema(): GraphQLSchema { +import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; +type ScalarConfigType = { + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + CustomName: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({ name: "SomeType", fields() { @@ -34,7 +44,10 @@ export function getSchema(): GraphQLSchema { } }); const CustomNameType: GraphQLScalarType = new GraphQLScalarType({ - name: "CustomName" + name: "CustomName", + serialize: config.scalars.CustomName.serialize, + parseValue: config.scalars.CustomName.parseValue, + parseLiteral: config.scalars.CustomName.parseLiteral }); return new GraphQLSchema({ types: [SomeTypeType, CustomNameType] diff --git a/src/tests/fixtures/field_values/CustomScalar.ts.expected b/src/tests/fixtures/field_values/CustomScalar.ts.expected index ffa23dab..e82bc651 100644 --- a/src/tests/fixtures/field_values/CustomScalar.ts.expected +++ b/src/tests/fixtures/field_values/CustomScalar.ts.expected @@ -22,10 +22,23 @@ type SomeType { hello: MyString } -- TypeScript -- -import { GraphQLSchema, GraphQLScalarType, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { +import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLObjectType } from "graphql"; +type ScalarConfigType = { + serialize: GraphQLScalarSerializer; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + MyString: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { const MyStringType: GraphQLScalarType = new GraphQLScalarType({ - name: "MyString" + name: "MyString", + serialize: config.scalars.MyString.serialize, + parseValue: config.scalars.MyString.parseValue, + parseLiteral: config.scalars.MyString.parseLiteral }); const SomeTypeType: GraphQLObjectType = new GraphQLObjectType({ name: "SomeType", From 49597cb5c3897881dc6b226d100a7a0baacb3176 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 30 Dec 2023 01:04:00 -0800 Subject: [PATCH 2/2] Custom scalars --- src/Errors.ts | 4 + src/Extractor.ts | 26 +++- src/GraphQLConstructor.ts | 3 +- src/codegen.ts | 129 +++++++++++------- src/metadataDirectives.ts | 2 +- .../arguments/CustomScalarArgument.ts | 2 +- .../CustomScalarArgument.ts.expected | 21 +-- .../custom_scalars/DefineCustomScalar.ts | 2 +- .../DefineCustomScalar.ts.expected | 21 +-- .../DefineCustomScalarWithDescription.ts | 2 +- ...ineCustomScalarWithDescription.ts.expected | 21 +-- .../DefineRenamedCustomScalar.ts | 2 +- .../DefineRenamedCustomScalar.ts.expected | 21 +-- .../fixtures/field_values/CustomScalar.ts | 2 +- .../field_values/CustomScalar.ts.expected | 21 +-- .../fixtures/locate/fieldOnScalar.invalid.ts | 2 +- .../locate/fieldOnScalar.invalid.ts.expected | 2 +- .../todo/RedefineBuiltinScalar.invalid.ts | 2 +- .../RedefineBuiltinScalar.invalid.ts.expected | 2 +- .../customScalars/index.ts | 56 ++++++++ .../customScalars/index.ts.expected | 70 ++++++++++ .../customScalars/schema.ts | 53 +++++++ src/tests/test.ts | 2 +- website/docs/04-docblock-tags/08-scalars.mdx | 61 +++++---- 24 files changed, 388 insertions(+), 141 deletions(-) create mode 100644 src/tests/integrationFixtures/customScalars/index.ts create mode 100644 src/tests/integrationFixtures/customScalars/index.ts.expected create mode 100644 src/tests/integrationFixtures/customScalars/schema.ts diff --git a/src/Errors.ts b/src/Errors.ts index 7db673c6..fe1c3f69 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -115,6 +115,10 @@ export function functionFieldNotNamedExport() { return `Expected a \`@${FIELD_TAG}\` function to be a named export. Grats needs to import resolver functions into it's generated schema module, so the resolver function must be a named export.`; } +export function customScalarTypeNotExported() { + return `Expected a \`@${SCALAR_TAG}\` type to be a named export. Grats needs to import custom scalar types into it's generated schema module, so the type must be a named export.`; +} + export function inputTypeNotLiteral() { return `\`@${INPUT_TAG}\` can only be used on type literals. e.g. \`type MyInput = { foo: string }\``; } diff --git a/src/Extractor.ts b/src/Extractor.ts index 3cf1e56f..177f6a4c 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -435,6 +435,16 @@ class Extractor { return node.name; } + typeAliasExportName(node: ts.TypeAliasDeclaration): ts.Identifier | null { + const exportKeyword = node.modifiers?.some((modifier) => { + return modifier.kind === ts.SyntaxKind.ExportKeyword; + }); + if (exportKeyword == null) { + return this.report(node.name, E.customScalarTypeNotExported()); + } + return node.name; + } + scalarTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag) { const name = this.entityName(node, tag); if (name == null) return null; @@ -442,8 +452,22 @@ class Extractor { const description = this.collectDescription(node); this.recordTypeName(node.name, name, "SCALAR"); + // Ensure the type is exported + const exportName = this.typeAliasExportName(node); + if (exportName == null) return null; + + const tsModulePath = relativePath(node.getSourceFile().fileName); + + const directives = [ + this.gql.exportedDirective(exportName, { + tsModulePath, + exportedFunctionName: exportName.text, + argCount: 0, + }), + ]; + this.definitions.push( - this.gql.scalarTypeDefinition(node, name, description), + this.gql.scalarTypeDefinition(node, name, directives, description), ); } diff --git a/src/GraphQLConstructor.ts b/src/GraphQLConstructor.ts index 33455416..beff67d3 100644 --- a/src/GraphQLConstructor.ts +++ b/src/GraphQLConstructor.ts @@ -228,6 +228,7 @@ export class GraphQLConstructor { scalarTypeDefinition( node: ts.Node, name: NameNode, + directives: readonly ConstDirectiveNode[] | null, description: StringValueNode | null, ): ScalarTypeDefinitionNode { return { @@ -235,7 +236,7 @@ export class GraphQLConstructor { loc: this._loc(node), description: description ?? undefined, name, - directives: undefined, + directives: this._optionalList(directives), }; } diff --git a/src/codegen.ts b/src/codegen.ts index 51b97146..464aa62f 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -155,43 +155,7 @@ class Codegen { return !PRIMITIVE_TYPE_NAMES.has(scalar.name); }); if (scalarTypes.length == 0) return null; - this._statements.push( - F.createTypeAliasDeclaration( - undefined, - SCALAR_CONFIG_TYPE_NAME, - [ - F.createTypeParameterDeclaration(undefined, "TInternal"), - F.createTypeParameterDeclaration(undefined, "TExternal"), - ], - F.createTypeLiteralNode([ - F.createPropertySignature( - undefined, - "serialize", - undefined, - this.graphQLTypeImport("GraphQLScalarSerializer", [ - F.createTypeReferenceNode("TExternal"), - ]), - ), - F.createPropertySignature( - undefined, - "parseValue", - undefined, - - this.graphQLTypeImport("GraphQLScalarValueParser", [ - F.createTypeReferenceNode("TInternal"), - ]), - ), - F.createPropertySignature( - undefined, - "parseLiteral", - undefined, - this.graphQLTypeImport("GraphQLScalarLiteralParser", [ - F.createTypeReferenceNode("TInternal"), - ]), - ), - ]), - ), - ); + this._statements.push(this.scalarConfigTypeDeclaration()); return F.createPropertySignature( undefined, SCHEMA_CONFIG_SCALARS_NAME, @@ -203,8 +167,9 @@ class Codegen { scalar.name, undefined, F.createTypeReferenceNode(SCALAR_CONFIG_TYPE_NAME, [ - F.createTypeReferenceNode("Date"), - F.createTypeReferenceNode("string"), + F.createTypeReferenceNode( + formatCustomScalarTypeName(scalar.name), + ), ]), ); }), @@ -212,6 +177,49 @@ class Codegen { ); } + scalarConfigTypeDeclaration(): ts.TypeAliasDeclaration { + return F.createTypeAliasDeclaration( + undefined, + SCALAR_CONFIG_TYPE_NAME, + [F.createTypeParameterDeclaration(undefined, "T")], + F.createTypeLiteralNode([ + F.createMethodSignature( + undefined, + "serialize", + undefined, + undefined, + [ + F.createParameterDeclaration( + undefined, + undefined, + "outputValue", + undefined, + F.createTypeReferenceNode("T"), + ), + ], + F.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ), + F.createPropertySignature( + undefined, + "parseValue", + undefined, + + this.graphQLTypeImport("GraphQLScalarValueParser", [ + F.createTypeReferenceNode("T"), + ]), + ), + F.createPropertySignature( + undefined, + "parseLiteral", + undefined, + this.graphQLTypeImport("GraphQLScalarLiteralParser", [ + F.createTypeReferenceNode("T"), + ]), + ), + ]), + ); + } + schemaConfigObject(): ts.ObjectLiteralExpression { return this.objectLiteral([ this.description(this._schema.description), @@ -502,7 +510,7 @@ class Codegen { varName, F.createNewExpression( this.graphQLImport("GraphQLScalarType"), - [], + [F.createTypeReferenceNode(formatCustomScalarTypeName(obj.name))], [this.customScalarTypeConfig(obj)], ), // We need to explicitly specify the type due to circular references in @@ -514,23 +522,42 @@ class Codegen { } customScalarTypeConfig(obj: GraphQLScalarType): ts.ObjectLiteralExpression { + const exported = fieldDirective(obj, EXPORTED_DIRECTIVE); + if (exported != null) { + const exportedMetadata = parseExportedDirective(exported); + const module = exportedMetadata.tsModulePath; + const funcName = exportedMetadata.exportedFunctionName; + const abs = resolveRelativePath(module); + const relative = stripExt( + path.relative(path.dirname(this._destination), abs), + ); + + const scalarTypeName = formatCustomScalarTypeName(obj.name); + this.import(`./${relative}`, [{ name: funcName, as: scalarTypeName }]); + } return this.objectLiteral([ this.description(obj.description), F.createPropertyAssignment("name", F.createStringLiteral(obj.name)), ...["serialize", "parseValue", "parseLiteral"].map((name) => { - return F.createPropertyAssignment( - name, + let func: ts.Expression = F.createPropertyAccessExpression( F.createPropertyAccessExpression( F.createPropertyAccessExpression( - F.createPropertyAccessExpression( - F.createIdentifier(SCHEMA_CONFIG_NAME), - SCHEMA_CONFIG_SCALARS_NAME, - ), - obj.name, + F.createIdentifier(SCHEMA_CONFIG_NAME), + SCHEMA_CONFIG_SCALARS_NAME, ), - name, + obj.name, ), + name, ); + if (name === "serialize") { + func = F.createAsExpression( + func, + this.graphQLTypeImport("GraphQLScalarSerializer", [ + F.createTypeReferenceNode(formatCustomScalarTypeName(obj.name)), + ]), + ); + } + return F.createPropertyAssignment(name, func); }), ]); } @@ -894,7 +921,7 @@ class Codegen { } function fieldDirective( - field: GraphQLField, + field: GraphQLField | GraphQLScalarType, name: string, ): ConstDirectiveNode | null { return field.astNode?.directives?.find((d) => d.name.value === name) ?? null; @@ -919,3 +946,7 @@ function formatResolverFunctionVarName( const field = fieldName[0].toUpperCase() + fieldName.slice(1); return `${parent}${field}Resolver`; } + +function formatCustomScalarTypeName(scalarName: string): string { + return `${scalarName}Type`; +} diff --git a/src/metadataDirectives.ts b/src/metadataDirectives.ts index 4e418c3c..403abb94 100644 --- a/src/metadataDirectives.ts +++ b/src/metadataDirectives.ts @@ -34,7 +34,7 @@ export const DIRECTIVES_AST: DocumentNode = parse(` ${TS_MODULE_PATH_ARG}: String!, ${EXPORTED_FUNCTION_NAME_ARG}: String! ${ARG_COUNT}: Int! - ) on FIELD_DEFINITION + ) on FIELD_DEFINITION | SCALAR directive @${KILLS_PARENT_ON_EXCEPTION_DIRECTIVE} on FIELD_DEFINITION `); diff --git a/src/tests/fixtures/arguments/CustomScalarArgument.ts b/src/tests/fixtures/arguments/CustomScalarArgument.ts index b4fad38f..089e376a 100644 --- a/src/tests/fixtures/arguments/CustomScalarArgument.ts +++ b/src/tests/fixtures/arguments/CustomScalarArgument.ts @@ -1,5 +1,5 @@ /** @gqlScalar */ -type MyString = string; +export type MyString = string; /** @gqlType */ export default class SomeType { diff --git a/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected b/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected index 9d990fd7..0b8c8f66 100644 --- a/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected +++ b/src/tests/fixtures/arguments/CustomScalarArgument.ts.expected @@ -2,7 +2,7 @@ INPUT ----------------- /** @gqlScalar */ -type MyString = string; +export type MyString = string; /** @gqlType */ export default class SomeType { @@ -16,27 +16,28 @@ export default class SomeType { OUTPUT ----------------- -- SDL -- -scalar MyString +scalar MyString @exported(tsModulePath: "grats/src/tests/fixtures/arguments/CustomScalarArgument.ts", functionName: "MyString", argCount: 0) type SomeType { hello(greeting: MyString!): String } -- TypeScript -- -import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; -type ScalarConfigType = { - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; +import { MyString as MyStringType } from "./CustomScalarArgument"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLScalarSerializer, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; }; export type SchemaConfigType = { scalars: { - MyString: ScalarConfigType; + MyString: ScalarConfigType; }; }; export function getSchema(config: SchemaConfigType): GraphQLSchema { - const MyStringType: GraphQLScalarType = new GraphQLScalarType({ + const MyStringType: GraphQLScalarType = new GraphQLScalarType({ name: "MyString", - serialize: config.scalars.MyString.serialize, + serialize: config.scalars.MyString.serialize as GraphQLScalarSerializer, parseValue: config.scalars.MyString.parseValue, parseLiteral: config.scalars.MyString.parseLiteral }); diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts index 8280d7b5..4d269944 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts @@ -5,4 +5,4 @@ class SomeType { } /** @gqlScalar */ -type MyUrl = string; +export type MyUrl = string; diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected index e9fda55b..109fcf70 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts.expected @@ -8,7 +8,7 @@ class SomeType { } /** @gqlScalar */ -type MyUrl = string; +export type MyUrl = string; ----------------- OUTPUT @@ -18,17 +18,18 @@ type SomeType { hello: String } -scalar MyUrl +scalar MyUrl @exported(tsModulePath: "grats/src/tests/fixtures/custom_scalars/DefineCustomScalar.ts", functionName: "MyUrl", argCount: 0) -- TypeScript -- -import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -type ScalarConfigType = { - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; +import { MyUrl as MyUrlType } from "./DefineCustomScalar"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType, GraphQLScalarSerializer } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; }; export type SchemaConfigType = { scalars: { - MyUrl: ScalarConfigType; + MyUrl: ScalarConfigType; }; }; export function getSchema(config: SchemaConfigType): GraphQLSchema { @@ -43,9 +44,9 @@ export function getSchema(config: SchemaConfigType): GraphQLSchema { }; } }); - const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ + const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ name: "MyUrl", - serialize: config.scalars.MyUrl.serialize, + serialize: config.scalars.MyUrl.serialize as GraphQLScalarSerializer, parseValue: config.scalars.MyUrl.parseValue, parseLiteral: config.scalars.MyUrl.parseLiteral }); diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts index 029b44ca..e0e0de2a 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts @@ -8,4 +8,4 @@ class SomeType { * Use this for URLs. * @gqlScalar */ -type MyUrl = string; +export type MyUrl = string; diff --git a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected index 5fb84f90..998ecdd2 100644 --- a/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts.expected @@ -11,7 +11,7 @@ class SomeType { * Use this for URLs. * @gqlScalar */ -type MyUrl = string; +export type MyUrl = string; ----------------- OUTPUT @@ -22,17 +22,18 @@ type SomeType { } """Use this for URLs.""" -scalar MyUrl +scalar MyUrl @exported(tsModulePath: "grats/src/tests/fixtures/custom_scalars/DefineCustomScalarWithDescription.ts", functionName: "MyUrl", argCount: 0) -- TypeScript -- -import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -type ScalarConfigType = { - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; +import { MyUrl as MyUrlType } from "./DefineCustomScalarWithDescription"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType, GraphQLScalarSerializer } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; }; export type SchemaConfigType = { scalars: { - MyUrl: ScalarConfigType; + MyUrl: ScalarConfigType; }; }; export function getSchema(config: SchemaConfigType): GraphQLSchema { @@ -47,10 +48,10 @@ export function getSchema(config: SchemaConfigType): GraphQLSchema { }; } }); - const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ + const MyUrlType: GraphQLScalarType = new GraphQLScalarType({ description: "Use this for URLs.", name: "MyUrl", - serialize: config.scalars.MyUrl.serialize, + serialize: config.scalars.MyUrl.serialize as GraphQLScalarSerializer, parseValue: config.scalars.MyUrl.parseValue, parseLiteral: config.scalars.MyUrl.parseLiteral }); diff --git a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts index a9da6105..e8de048b 100644 --- a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts +++ b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts @@ -5,4 +5,4 @@ class SomeType { } /** @gqlScalar CustomName */ -type MyUrl = string; +export type MyUrl = string; diff --git a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected index a20734be..40f15ff1 100644 --- a/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected +++ b/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts.expected @@ -8,7 +8,7 @@ class SomeType { } /** @gqlScalar CustomName */ -type MyUrl = string; +export type MyUrl = string; ----------------- OUTPUT @@ -18,17 +18,18 @@ type SomeType { hello: String } -scalar CustomName +scalar CustomName @exported(tsModulePath: "grats/src/tests/fixtures/custom_scalars/DefineRenamedCustomScalar.ts", functionName: "MyUrl", argCount: 0) -- TypeScript -- -import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType } from "graphql"; -type ScalarConfigType = { - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; +import { MyUrl as CustomNameType } from "./DefineRenamedCustomScalar"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLScalarType, GraphQLScalarSerializer } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; }; export type SchemaConfigType = { scalars: { - CustomName: ScalarConfigType; + CustomName: ScalarConfigType; }; }; export function getSchema(config: SchemaConfigType): GraphQLSchema { @@ -43,9 +44,9 @@ export function getSchema(config: SchemaConfigType): GraphQLSchema { }; } }); - const CustomNameType: GraphQLScalarType = new GraphQLScalarType({ + const CustomNameType: GraphQLScalarType = new GraphQLScalarType({ name: "CustomName", - serialize: config.scalars.CustomName.serialize, + serialize: config.scalars.CustomName.serialize as GraphQLScalarSerializer, parseValue: config.scalars.CustomName.parseValue, parseLiteral: config.scalars.CustomName.parseLiteral }); diff --git a/src/tests/fixtures/field_values/CustomScalar.ts b/src/tests/fixtures/field_values/CustomScalar.ts index e2b001eb..9bc10f33 100644 --- a/src/tests/fixtures/field_values/CustomScalar.ts +++ b/src/tests/fixtures/field_values/CustomScalar.ts @@ -1,5 +1,5 @@ /** @gqlScalar */ -type MyString = string; +export type MyString = string; /** @gqlType */ export default class SomeType { diff --git a/src/tests/fixtures/field_values/CustomScalar.ts.expected b/src/tests/fixtures/field_values/CustomScalar.ts.expected index e82bc651..87c8e25b 100644 --- a/src/tests/fixtures/field_values/CustomScalar.ts.expected +++ b/src/tests/fixtures/field_values/CustomScalar.ts.expected @@ -2,7 +2,7 @@ INPUT ----------------- /** @gqlScalar */ -type MyString = string; +export type MyString = string; /** @gqlType */ export default class SomeType { @@ -16,27 +16,28 @@ export default class SomeType { OUTPUT ----------------- -- SDL -- -scalar MyString +scalar MyString @exported(tsModulePath: "grats/src/tests/fixtures/field_values/CustomScalar.ts", functionName: "MyString", argCount: 0) type SomeType { hello: MyString } -- TypeScript -- -import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLObjectType } from "graphql"; -type ScalarConfigType = { - serialize: GraphQLScalarSerializer; - parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; +import { MyString as MyStringType } from "./CustomScalar"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLScalarType, GraphQLScalarSerializer, GraphQLObjectType } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; }; export type SchemaConfigType = { scalars: { - MyString: ScalarConfigType; + MyString: ScalarConfigType; }; }; export function getSchema(config: SchemaConfigType): GraphQLSchema { - const MyStringType: GraphQLScalarType = new GraphQLScalarType({ + const MyStringType: GraphQLScalarType = new GraphQLScalarType({ name: "MyString", - serialize: config.scalars.MyString.serialize, + serialize: config.scalars.MyString.serialize as GraphQLScalarSerializer, parseValue: config.scalars.MyString.parseValue, parseLiteral: config.scalars.MyString.parseLiteral }); diff --git a/src/tests/fixtures/locate/fieldOnScalar.invalid.ts b/src/tests/fixtures/locate/fieldOnScalar.invalid.ts index f4e509de..9f18a922 100644 --- a/src/tests/fixtures/locate/fieldOnScalar.invalid.ts +++ b/src/tests/fixtures/locate/fieldOnScalar.invalid.ts @@ -1,3 +1,3 @@ // Locate: Date.name /** @gqlScalar */ -type Date = string; +export type Date = string; diff --git a/src/tests/fixtures/locate/fieldOnScalar.invalid.ts.expected b/src/tests/fixtures/locate/fieldOnScalar.invalid.ts.expected index 52515ed4..d8472fa0 100644 --- a/src/tests/fixtures/locate/fieldOnScalar.invalid.ts.expected +++ b/src/tests/fixtures/locate/fieldOnScalar.invalid.ts.expected @@ -3,7 +3,7 @@ INPUT ----------------- // Locate: Date.name /** @gqlScalar */ -type Date = string; +export type Date = string; ----------------- OUTPUT diff --git a/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts b/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts index c3269f96..bea11195 100644 --- a/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts +++ b/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts @@ -1,2 +1,2 @@ /** @gqlScalar String */ -type MyUrl = string; +export type MyUrl = string; diff --git a/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts.expected b/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts.expected index 230e1881..3f410dac 100644 --- a/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts.expected +++ b/src/tests/fixtures/todo/RedefineBuiltinScalar.invalid.ts.expected @@ -2,7 +2,7 @@ INPUT ----------------- /** @gqlScalar String */ -type MyUrl = string; +export type MyUrl = string; ----------------- OUTPUT diff --git a/src/tests/integrationFixtures/customScalars/index.ts b/src/tests/integrationFixtures/customScalars/index.ts new file mode 100644 index 00000000..18c75204 --- /dev/null +++ b/src/tests/integrationFixtures/customScalars/index.ts @@ -0,0 +1,56 @@ +import { Maybe } from "@graphql-tools/utils"; +import { ValueNode } from "graphql"; +import { ObjMap } from "graphql/jsutils/ObjMap"; + +/** @gqlType */ +type Query = unknown; + +/** + * @gqlScalar + */ +export type DateTime = Date; + +/** + * @gqlField + */ +export function echo(_: Query, args: { in: DateTime }): DateTime { + return args.in; +} + +/** + * @gqlField + */ +export function now(_: Query): DateTime { + return new Date(); +} + +export const config = { + scalars: { + DateTime: { + serialize: (value: DateTime): number => value.getTime(), + parseValue(value: unknown): DateTime { + if (typeof value !== "number") throw new Error("Date is not a number"); + return new Date(value); + }, + parseLiteral( + ast: ValueNode, + _variables?: Maybe>, + ): DateTime { + if (ast.kind !== "IntValue") throw new Error("Date is not IntValue"); + return new Date(ast.value); + }, + }, + }, +}; + +export const query = ` + query SomeQuery($in: DateTime!) { + echo(in: 1703926606365) + echoVar: echo(in: $in) + now + } + `; + +export const variables = { + in: 1703926606365, +}; diff --git a/src/tests/integrationFixtures/customScalars/index.ts.expected b/src/tests/integrationFixtures/customScalars/index.ts.expected new file mode 100644 index 00000000..563fc2ad --- /dev/null +++ b/src/tests/integrationFixtures/customScalars/index.ts.expected @@ -0,0 +1,70 @@ +----------------- +INPUT +----------------- +import { Maybe } from "@graphql-tools/utils"; +import { ValueNode } from "graphql"; +import { ObjMap } from "graphql/jsutils/ObjMap"; + +/** @gqlType */ +type Query = unknown; + +/** + * @gqlScalar + */ +export type DateTime = Date; + +/** + * @gqlField + */ +export function echo(_: Query, args: { in: DateTime }): DateTime { + return args.in; +} + +/** + * @gqlField + */ +export function now(_: Query): DateTime { + return new Date(); +} + +export const config = { + scalars: { + DateTime: { + serialize: (value: DateTime): number => value.getTime(), + parseValue(value: unknown): DateTime { + if (typeof value !== "number") throw new Error("Date is not a number"); + return new Date(value); + }, + parseLiteral( + ast: ValueNode, + _variables?: Maybe>, + ): DateTime { + if (ast.kind !== "IntValue") throw new Error("Date is not IntValue"); + return new Date(ast.value); + }, + }, + }, +}; + +export const query = ` + query SomeQuery($in: DateTime!) { + echo(in: 1703926606365) + echoVar: echo(in: $in) + now + } + `; + +export const variables = { + in: 1703926606365, +}; + +----------------- +OUTPUT +----------------- +{ + "data": { + "echo": null, // TODO: That's not write + "echoVar": 1703926606365, + "now": 1703926988440 // TODO: Remove this + } +} \ No newline at end of file diff --git a/src/tests/integrationFixtures/customScalars/schema.ts b/src/tests/integrationFixtures/customScalars/schema.ts new file mode 100644 index 00000000..fd24bc64 --- /dev/null +++ b/src/tests/integrationFixtures/customScalars/schema.ts @@ -0,0 +1,53 @@ +import { DateTime as DateTimeType } from "./index"; +import { echo as queryEchoResolver } from "./index"; +import { now as queryNowResolver } from "./index"; +import { GraphQLScalarValueParser, GraphQLScalarLiteralParser, GraphQLSchema, GraphQLObjectType, GraphQLScalarType, GraphQLScalarSerializer, GraphQLNonNull } from "graphql"; +type ScalarConfigType = { + serialize(outputValue: T): any; + parseValue: GraphQLScalarValueParser; + parseLiteral: GraphQLScalarLiteralParser; +}; +export type SchemaConfigType = { + scalars: { + DateTime: ScalarConfigType; + }; +}; +export function getSchema(config: SchemaConfigType): GraphQLSchema { + const DateTimeType: GraphQLScalarType = new GraphQLScalarType({ + name: "DateTime", + serialize: config.scalars.DateTime.serialize as GraphQLScalarSerializer, + parseValue: config.scalars.DateTime.parseValue, + parseLiteral: config.scalars.DateTime.parseLiteral + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + echo: { + name: "echo", + type: DateTimeType, + args: { + in: { + name: "in", + type: new GraphQLNonNull(DateTimeType) + } + }, + resolve(source, args) { + return queryEchoResolver(source, args); + } + }, + now: { + name: "now", + type: DateTimeType, + resolve(source) { + return queryNowResolver(source); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType, DateTimeType] + }); +} diff --git a/src/tests/test.ts b/src/tests/test.ts index 2be95c8f..d4b3efcd 100644 --- a/src/tests/test.ts +++ b/src/tests/test.ts @@ -196,7 +196,7 @@ const testDirs = [ const schemaModule = await import(schemaPath); - const actualSchema = schemaModule.getSchema(); + const actualSchema = schemaModule.getSchema(server.config); const schemaDiff = compareSchemas(actualSchema, schemaResult.value); diff --git a/website/docs/04-docblock-tags/08-scalars.mdx b/website/docs/04-docblock-tags/08-scalars.mdx index f6f01d8f..1fed5124 100644 --- a/website/docs/04-docblock-tags/08-scalars.mdx +++ b/website/docs/04-docblock-tags/08-scalars.mdx @@ -9,9 +9,13 @@ GraphQL custom sclars can be defined by placing a `@gqlScalar` docblock directly * A description of my custom scalar. * @gqlScalar */ -type MyCustomString = string; +export type MyCustomString = string; ``` +:::note +Grats requires that you export your scalar types so that it may import them into your schema module to define types for the scalars's serialization/deserialization functions. +::: + ## Built-In Scalars :::note @@ -35,45 +39,44 @@ class Math { ## Serialization and Parsing of Custom Scalars -Grats does not ([yet](https://github.com/captbaritone/grats/issues/66)) support a first-class way to define serialization and parsing logic for custom scalars. However, you can do this manually by modifying the schema after it is generated. +When you define a custom scalar, you aslo need to inform the GraphQL executor how to serialize the data (convert the value into something JSON serializable) and how to parse the data (convert a value provided as a variable into the value expected by field arguments). The tree functions you must define are: + +* `serialize` Converts the return value of a resolver into a JSON serializable value. +* `parseValue` Converts the value of a variable into the value expected by a field argument. +* `parseLiteral` Converts the value of a literal (included in the query text) into the value expected by a field argument. -For example if you had a `Date` type in your schema: +Grats ensures you provide serializaiton/deseiralization functions for each of your custom scalars by requiring that you pass them when you call `getSchema`. + +### Custom Scalars Example: + +For example if you define a `Date` custom scalar type in your code: ```ts title="scalars.ts" /** @gqlScalar Date */ export type GqlDate = Date; ``` -To define a custom `serialize/parseValue/parseLiteral` transform for this type, which serialized the data as a Unix timestamp, you could do the following: +The `getSchema` function that Grats generates will require that you pass a config object with a `scalars` property, which is an object with a `Date` property, which is an object specifying `serialize`/`parseValue`/`parseLiteral` transformation functions: ```ts title="server.ts" import { getSchema } from "./schema"; // Generated by Grats import { GqlDate } from "./scalars"; -const schema = getSchema(); - -const date = schema.getType("Date") as GraphQLScalarType; - -date.serialize = (value) => { - if (!(value instanceof Date)) { - throw new Error("Date.serialize: value is not a Date object"); - } - return value.getTime(); -}; -date.parseValue = (value) => { - if (typeof value !== "number") { - throw new Error("Date.parseValue: value is not a number"); - } - return new Date(value); -}; -date.parseLiteral = (ast) => { - if (!(ast.kind === "IntValue" || ast.kind === "StringValue")) { - throw new Error( - "Date.parseLiteral: ast.kind is not IntValue or StringValue", - ); - } - return new Date(Number(ast.value)); -}; - + const schema = getSchema({ + scalars: { + Date: { + serialize: (value: GqlDate): number => value.getTime(), + parseValue(value: unknown): GqlDate { + if (typeof value !== "number") throw new Error("Date is not a number"); + return new Date(value); + }, + parseLiteral(ast: ValueNode): GqlDate { + if (ast.kind !== "IntValue") throw new Error("Date is not IntValue"); + return new Date(Number(ast.value)); + }, + }, + }, +}); // ... Continue on, using the schema to create a GraphQL server ``` +