diff --git a/.changeset/three-rules-fail.md b/.changeset/three-rules-fail.md new file mode 100644 index 000000000..345580c58 --- /dev/null +++ b/.changeset/three-rules-fail.md @@ -0,0 +1,10 @@ +--- +'@pothos/core': minor +--- + +Add `withScalar` method to the schema builder to allow inference of Scalar typescript types from +`GraphQLScalarType` scalars + +```typescript +const builder = new SchemaBuilder({}).withScalars({ Date: CustomDateScalar }); +``` diff --git a/packages/core/package.json b/packages/core/package.json index ceee51935..13cd5e0b6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -52,7 +52,7 @@ "devDependencies": { "@pothos/test-utils": "workspace:*", "graphql": "16.8.1", - "graphql-scalars": "^1.22.2", + "graphql-scalars": "^1.22.4", "graphql-tag": "^2.12.6" }, "gitHead": "9dfe52f1975f41a111e01bf96a20033a914e2acc" diff --git a/packages/core/src/builder.ts b/packages/core/src/builder.ts index 5d0f07afa..3e827e1bd 100644 --- a/packages/core/src/builder.ts +++ b/packages/core/src/builder.ts @@ -591,8 +591,8 @@ export default class SchemaBuilder { const [options = {}] = args; const { directives, extensions } = options; - const scalars = [GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean]; - scalars.forEach((scalar) => { + const builtInScalars = [GraphQLID, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean]; + builtInScalars.forEach((scalar) => { if (!this.configStore.hasConfig(scalar.name as OutputType)) { this.addScalarType(scalar.name as ScalarName, scalar, {}); } @@ -621,4 +621,43 @@ export default class SchemaBuilder { ? processedSchema : lexicographicSortSchema(processedSchema); } + + + withScalar< + const TName extends string, + TInternal, + TExternal, + TScalarType extends GraphQLScalarType, + TTypes extends Types & { + Scalars: { [K in TName]: { Input: TInternal; Output: TExternal; }} + outputShapes: { [K in TName]: TInternal }, + inputShapes: {[K in TName]: TInternal}, + } + >( + name: TName, + scalar: TScalarType, + options?: Omit< + PothosSchemaTypes.ScalarTypeOptions, OutputShape>, + 'serialize' + > & { + serialize?: GraphQLScalarSerializer>; + }, + ) { + const builder = this as unknown as SchemaBuilder; + const config = scalar.toConfig();`` + builder.scalarType(name, { + ...config, + ...options, + extensions: { + ...config.extensions, + ...options?.extensions, + }, + } as PothosSchemaTypes.ScalarTypeOptions< + TTypes, + InputShape, + ParentShape + >); + return builder + } + } diff --git a/packages/core/tests/scalars.test.ts b/packages/core/tests/scalars.test.ts new file mode 100644 index 000000000..75ded50d6 --- /dev/null +++ b/packages/core/tests/scalars.test.ts @@ -0,0 +1,390 @@ +import { execute, GraphQLBoolean, GraphQLScalarType } from 'graphql'; +import { + DateTimeResolver, + NonNegativeIntResolver, + HexColorCodeResolver, + GraphQLEmailAddress, + GraphQLHexColorCode, + GraphQLISBN, + GraphQLBigInt, + GraphQLRGB, + GraphQLDate, + GraphQLJSON, + GraphQLJSONObject +} from 'graphql-scalars'; +import gql from 'graphql-tag'; +import SchemaBuilder from '../src'; + +const PositiveIntResolver = new GraphQLScalarType({ + name: 'PositiveInt', + serialize: (n) => n as number, + parseValue: (n) => { + if (typeof n !== 'number') { + throw new TypeError('Value must be a number'); + } + + if (n >= 0) { + return n; + } + + throw new TypeError('Value must be positive'); + }, +}); + +enum Diet { + HERBIVOROUS, + CARNIVOROUS, + OMNIVORIOUS, +} + +class Animal { + diet: Diet; + + constructor(diet: Diet) { + this.diet = diet; + } +} + +class Kiwi extends Animal { + name: string; + birthday: Date; + heightInMeters: number; + + constructor(name: string, birthday: Date, heightInMeters: number) { + super(Diet.HERBIVOROUS); + + this.name = name; + this.birthday = birthday; + this.heightInMeters = heightInMeters; + } +} + +describe('scalars', () => { + it('when a scalar is added withScalar, the scalartype is added', () => { + const builder = new SchemaBuilder({}).withScalar('PositiveInt', PositiveIntResolver); + builder.queryType(); + builder.queryFields((t) => ({ + positiveInt: t.field({ + type: 'PositiveInt', + args: { v: t.arg.int({ required: true }) }, + resolve: (_root, args) => args.v, + }), + })); + + const schema = builder.toSchema(); + expect(() => + execute({ + schema, + document: gql`query { positiveInt("hello") }`, + }), + ).toThrow('Expected Name, found String "hello"'); + }); + + it('when scalars are added using withScalar, the Objects from the user schema are kept', async () => { + const builder = new SchemaBuilder<{ + Objects: { + Example: { n: number }; + }; + }>({}).withScalar('PositiveInt', PositiveIntResolver); + + const Example = builder.objectType('Example', { + fields: (t) => ({ + n: t.expose('n', { + type: 'PositiveInt', + }), + }), + }); + + builder.queryType({ + fields: (t) => ({ + example: t.field({ + type: Example, + resolve: () => ({ n: 1 }), + }), + }), + }); + const schema = builder.toSchema(); + + const result = await execute({ + schema, + document: gql` + query { + example { + n + } + } + `, + }); + expect(result.data).toEqual({ example: { n: 1 } }); + }); + + it('when scalars are added using withScalar, the Interfaces from the user schema are kept', async () => { + const builder = new SchemaBuilder<{ + Interfaces: { + Animal: Animal; + }; + }>({}).withScalar('NonNegativeInt', NonNegativeIntResolver); + + builder.enumType(Diet, { name: 'Diet' }); + + builder.interfaceType('Animal', { + fields: (t) => ({ + diet: t.expose('diet', { + exampleRequiredOptionFromPlugin: true, + type: Diet, + }), + }), + }); + + builder.objectType(Kiwi, { + name: 'Kiwi', + interfaces: ['Animal'], + isTypeOf: (value) => value instanceof Kiwi, + description: 'Long beaks, little legs, rounder than you.', + fields: (t) => ({ + name: t.exposeString('name', {}), + age: t.int({ + resolve: (parent) => 5, // hard coded so test don't break over time + }), + }), + }); + + builder.queryType({ + fields: (t) => ({ + kiwi: t.field({ + type: 'Animal', + resolve: () => new Kiwi('TV Kiwi', new Date(Date.UTC(1975, 0, 1)), 0.5), + }), + }), + }); + + const schema = builder.toSchema(); + const result = await execute({ + schema, + document: gql` + query { + kiwi { + name + age + heightInMeters + } + } + `, + }); + expect(result.data).toEqual({ kiwi: { name: 'TV Kiwi', age: 5 } }); + }); + + it('when scalars are added using withScalar, the Context from the user schema are kept', async () => { + const builder = new SchemaBuilder<{ + Context: { name: string }; + }>({}).withScalar('NonNegativeInt', NonNegativeIntResolver); + + builder.queryType({ + fields: (t) => ({ + name: t.field({ + type: 'String', + resolve: (_root, _args, context) => context.name, + }), + }), + }); + + const schema = builder.toSchema(); + const result = await execute({ + schema, + document: gql` + query { + name + } + `, + contextValue: { name: 'Hello' }, + }); + expect(result.data).toEqual({ name: 'Hello' }); + }); + + it('when scalars are added using withScalar, the DefaultFieldNullability from the user schema are kept', async () => { + const builder = new SchemaBuilder<{ + DefaultFieldNullability: true; + }>({ + defaultFieldNullability: true, + }).withScalar('NonNegativeInt', NonNegativeIntResolver); + + builder.queryType({ + fields: (t) => ({ + name: t.field({ + type: 'String', + resolve: () => null, + }), + }), + }); + + const schema = builder.toSchema(); + const result = await execute({ + schema, + document: gql` + query { + name + } + `, + }); + expect(result.data).toEqual({ name: null }); + }); + + it('when scalars are added using withScalar, the DefaultInputFieldRequiredness from the user schema are kept', async () => { + const builder = new SchemaBuilder<{ + DefaultInputFieldRequiredness: true; + }>({ + defaultInputFieldRequiredness: true, + }).withScalar('NonNegativeInt', NonNegativeIntResolver); + + builder.queryType({ + fields: (t) => ({ + example: t.field({ + type: 'Int', + args: { + v: t.arg.int(), + }, + // Would be a type error here if didn't work + resolve: (_root, args) => args.v, + }), + }), + }); + + const schema = builder.toSchema(); + const result = await execute({ + schema, + document: gql` + query { + example(v: 3) + } + `, + }); + expect(result.data).toEqual({ example: 3 }); + }); + + it('when scalars are added withScalar, scalars can still be manually typed', () => { + const builder = new SchemaBuilder<{ + Scalars: { + PositiveInt: { Input: number; Output: number }; + }; + }>({}).withScalar('DateTime', DateTimeResolver); + + builder.addScalarType('PositiveInt', PositiveIntResolver, {}); + + builder.objectRef<{}>('Example').implement({ + fields: (t) => ({ + // Manual typing + positiveInt: t.field({ + type: 'PositiveInt', + resolve: () => 1, + }), + // Inferred + datetime: t.field({ + type: 'DateTime', + resolve: () => new Date(), + }), + }), + }); + + expect(builder).toBeDefined(); + }); + + it('when the scalar has internal types the, scalar types are infered are possible', () => { + const builder = new SchemaBuilder({}) + .withScalar('DateTime', DateTimeResolver) + .withScalar('PositiveInt', PositiveIntResolver); + + builder.objectRef<{}>('Example').implement({ + fields: (t) => ({ + positiveInt: t.field({ + type: 'PositiveInt', + resolve: () => 1, + }), + datetime: t.field({ + type: 'DateTime', + resolve: () => new Date(), + }), + }), + }); + + expect(builder).toBeDefined(); + }); + + it('when a scalar has options', async () => { + const builder = new SchemaBuilder({}) + .withScalar('HexColor', HexColorCodeResolver) + .withScalar('HexColorNoHash', HexColorCodeResolver, { + serialize: (v) => (v as string).slice(1), + }); + + const resolve = () => '#BADA55'; + + builder.queryType(); + builder.queryFields((t) => ({ + hex: t.field({ type: 'HexColor', resolve }), + hexNoHash: t.field({ type: 'HexColorNoHash', resolve }), + })); + + const schema = builder.toSchema(); + const result = await execute({ + schema, + document: gql` + query { + hex + hexNoHash + } + `, + }); + expect(result.data).to.deep.eq({ hex: '#BADA55', hexNoHash: 'BADA55' }); + }); + + it('works will lots of scalars', async () => { + const builder = new SchemaBuilder({}) + .withScalar('Email', GraphQLEmailAddress) + .withScalar('HexColor', GraphQLHexColorCode) + .withScalar('Boolean', GraphQLBoolean) + .withScalar('ISBN', GraphQLISBN) + .withScalar("BigInt", GraphQLBigInt) + .withScalar("RGB", GraphQLRGB) + .withScalar("Date", GraphQLDate) + .withScalar("JSONObject", GraphQLJSONObject) + + builder.queryType(); + builder.queryFields((t) => ({ + email: t.field({ type: 'Email', resolve: () => 'hello@example.com' }), + hex: t.field({ type: 'HexColor', resolve: () => '#BADA55' }), + boolean: t.field({ type: 'Boolean', resolve: () => true }), + isbn: t.field({ type: 'ISBN', resolve: () => '9780008117450' }), + bigInt: t.field({ type: 'BigInt', resolve: () => BigInt(Number.MAX_SAFE_INTEGER) + BigInt(Number.MAX_SAFE_INTEGER) }), + rgb: t.field({ type: 'RGB', resolve: () => 'rgb(100,100,100)' }), + date: t.field({ type: 'Date', resolve: () => new Date('2000-01-01') }), + jsonObject: t.field({ type: 'JSONObject', resolve: () => ({hello: 'there'}) }), + })); + + const result = await execute({ + schema: builder.toSchema(), + document: gql` + query { + email + hex + boolean + isbn + bigInt + byte + rgb + date, + jsonObject + } + `, + }); + expect(result.data).to.deep.eq({ + boolean: true, + email: 'hello@example.com', + hex: '#BADA55', + isbn: '9780008117450', + bigInt: "18014398509481982", + rgb: 'rgb(100,100,100)', + date: "2000-01-01", + jsonObject: {hello: 'there'} + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66e6f7fc5..015454c19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -582,8 +582,8 @@ importers: specifier: 16.8.1 version: 16.8.1 graphql-scalars: - specifier: ^1.22.2 - version: 1.22.2(graphql@16.8.1) + specifier: ^1.22.4 + version: 1.22.4(graphql@16.8.1) graphql-tag: specifier: ^2.12.6 version: 2.12.6(graphql@16.8.1) @@ -3982,7 +3982,7 @@ packages: '@graphql-tools/utils': 8.9.0(graphql@16.8.1) dataloader: 2.1.0 graphql: 16.8.1 - tslib: 2.4.1 + tslib: 2.6.2 value-or-promise: 1.0.11 dev: false @@ -4052,7 +4052,7 @@ packages: dependencies: graphql: 16.8.1 lodash.sortby: 4.7.0 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /@graphql-tools/executor-graphql-ws@1.1.0(graphql@16.8.1): @@ -4273,7 +4273,7 @@ packages: graphql: 16.8.1 || ^16.5.0 dependencies: graphql: 16.8.1 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /@graphql-tools/prisma-loader@8.0.1(@types/node@20.8.2)(graphql@16.8.1): @@ -4332,7 +4332,7 @@ packages: '@ardatan/relay-compiler': 12.0.0(graphql@16.8.1) '@graphql-tools/utils': 10.0.6(graphql@16.8.1) graphql: 16.8.1 - tslib: 2.5.3 + tslib: 2.6.2 transitivePeerDependencies: - encoding - supports-color @@ -4426,7 +4426,7 @@ packages: graphql: 16.8.1 || ^16.5.0 dependencies: graphql: 16.8.1 - tslib: 2.4.1 + tslib: 2.6.2 dev: false /@graphql-tools/utils@9.2.1(graphql@16.8.1): @@ -8153,7 +8153,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /camelcase-css@2.0.1: @@ -8186,7 +8186,7 @@ packages: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: no-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 upper-case-first: 2.0.2 dev: true @@ -8278,7 +8278,7 @@ packages: path-case: 3.0.4 sentence-case: 3.0.4 snake-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /char-regex@1.0.2: @@ -8591,7 +8591,7 @@ packages: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} dependencies: no-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 upper-case: 2.0.2 dev: true @@ -9264,7 +9264,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /dotenv@16.3.1: @@ -11235,6 +11235,16 @@ packages: graphql: 16.8.1 tslib: 2.6.2 + /graphql-scalars@1.22.4(graphql@16.8.1): + resolution: {integrity: sha512-ILnv7jq5VKHLUyoaTFX7lgYrjCd6vTee9i8/B+D4zJKJT5TguOl0KkpPEbXHjmeor8AZYrVsrYUHdqRBMX1pjA==} + engines: {node: '>=10'} + peerDependencies: + graphql: 16.8.1 || ^16.5.0 + dependencies: + graphql: 16.8.1 + tslib: 2.6.2 + dev: true + /graphql-shield@7.6.5(graphql-middleware@6.1.35)(graphql@16.8.1): resolution: {integrity: sha512-etbzf7UIhQW6vadn/UR+ds0LJOceO8ITDXwbUkQMlP2KqPgSKTZRE2zci+AUfqP+cpV9zDQdbTJfPfW5OCEamg==} peerDependencies: @@ -11516,7 +11526,7 @@ packages: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} dependencies: capital-case: 1.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /highlight.js@11.8.0: @@ -12001,7 +12011,7 @@ packages: /is-lower-case@2.0.2: resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /is-map@2.0.2: @@ -12141,7 +12151,7 @@ packages: /is-upper-case@2.0.2: resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /is-weakmap@2.0.1: @@ -13096,13 +13106,13 @@ packages: /lower-case-first@2.0.2: resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /lowercase-keys@1.0.0: @@ -14136,7 +14146,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /node-abort-controller@3.1.1: @@ -14557,7 +14567,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /parent-module@1.0.1: @@ -14638,7 +14648,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /path-browserify@0.0.1: @@ -14653,7 +14663,7 @@ packages: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: dot-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /path-equal@1.2.5: @@ -15858,7 +15868,7 @@ packages: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} dependencies: no-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 upper-case-first: 2.0.2 dev: true @@ -16013,7 +16023,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.5.3 + tslib: 2.6.2 dev: true /socks-proxy-agent@7.0.0: @@ -16141,7 +16151,7 @@ packages: /sponge-case@1.0.1: resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /sprintf-js@1.0.3: @@ -16561,7 +16571,7 @@ packages: /swap-case@2.0.2: resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /symbol-observable@4.0.0: @@ -16734,7 +16744,7 @@ packages: /title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /titleize@3.0.0: @@ -17447,13 +17457,13 @@ packages: /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} dependencies: - tslib: 2.5.3 + tslib: 2.6.2 dev: true /uri-js@4.4.1: diff --git a/website/pages/docs/guide/scalars.mdx b/website/pages/docs/guide/scalars.mdx index b4fb9875b..6a266475d 100644 --- a/website/pages/docs/guide/scalars.mdx +++ b/website/pages/docs/guide/scalars.mdx @@ -14,8 +14,23 @@ export const getStaticProps = () => ({ props: { nav: buildNav() } }); # Scalars +## Adding GraphQL Scalars that have type information + +To add a custom scalar that has been implemented as a GraphQLScalarType from +[graphql-js](https://github.com/graphql/graphql-js), when the GraphQLScalar has `TInteral` and +`TExternal` type information this can be inferred using `withScalars. + +```typescript +import { DateResolver, JSONResolver } from 'graphql-scalars'; + +const builder = new SchemaBuilder({}) + .withScalar('Date', DateResolver) + .withScalar('JSON', JSONResolver); +``` + ## Adding Custom GraphQL Scalars + To add a custom scalar that has been implemented as GraphQLScalar from [graphql-js](https://github.com/graphql/graphql-js) you need to provide some type information in SchemaTypes generic parameter of the builder: @@ -116,6 +131,33 @@ builder.scalarType('PositiveInt', { }); ``` +or using `graphql` and `withScalars` + +```typescript +import { GraphQLScalarType, Kind } from 'graphql'; + +const PositiveInt = new GraphQLScalarType({ + name: 'PositiveInt', + serialize: (n) => { + if (typeof n !== 'number') { + throw new TypeError('Value must be number'); + } + return n; + }, + parseValue: (n) => { + if (typeof n !== 'number') { + throw new TypeError('Value must be number'); + } + if (n >= 0) { + return n; + } + throw new Error('Value must be positive'); + }, +}); + +const builder = new SchemaBuilder({}).withScalars({ PositiveInt }); +``` + ## Using scalars ```typescript