Skip to content

Commit 34c9e25

Browse files
authored
feat(ts-client): string support for custom scalars (#742)
1 parent 18d5c3f commit 34c9e25

File tree

17 files changed

+255
-154
lines changed

17 files changed

+255
-154
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
"types": "./build/entrypoints/alpha/schema.d.ts",
2525
"default": "./build/entrypoints/alpha/schema.js"
2626
}
27+
},
28+
"./alpha/schema/scalars": {
29+
"import": {
30+
"types": "./build/entrypoints/alpha/scalars.d.ts",
31+
"default": "./build/entrypoints/alpha/scalars.js"
32+
}
2733
}
2834
},
2935
"packageManager": "[email protected]",

src/ResultSet/ResultSet.test-d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/ban-types */
22

33
import { expectTypeOf, test } from 'vitest'
4-
import type * as Schema from '../../tests/ts/_/schema.js'
4+
import type * as Schema from '../../tests/ts/_/schema/Schema.js'
55
import type { SelectionSet } from '../SelectionSet/__.js'
66
import type { ResultSet } from './__.js'
77

@@ -22,6 +22,9 @@ test(`general`, () => {
2222
expectTypeOf<RS<{ id: true; string: false }>>().toEqualTypeOf<{ id: null | string }>()
2323
expectTypeOf<RS<{ id: true; string: 0 }>>().toEqualTypeOf<{ id: null | string }>()
2424
expectTypeOf<RS<{ id: true; string: undefined }>>().toEqualTypeOf<{ id: null | string }>()
25+
26+
// Custom Scalar
27+
expectTypeOf<RS<{ date: true }>>().toEqualTypeOf<{ date: null | string }>()
2528

2629
// List
2730
expectTypeOf<RS<{ listIntNonNull: true }>>().toEqualTypeOf<{ listIntNonNull: number[] }>()

src/Schema/Index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ export interface Index {
1212
unions: {
1313
Union: null | Union
1414
}
15-
scalars: object
1615
}

src/Schema/NamedType/Scalar/Scalar.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { nativeScalarConstructors } from './nativeConstructors.js'
44

5+
export { nativeScalarConstructors } from './nativeConstructors.js'
6+
57
export const ScalarKind = `Scalar`
68

79
export type ScalarKind = typeof ScalarKind
@@ -44,4 +46,17 @@ export type Boolean = typeof Boolean
4446

4547
export type Float = typeof Float
4648

47-
export type Any = String | Int | Boolean | ID | Float
49+
export const Scalars = {
50+
String,
51+
ID,
52+
Int,
53+
Float,
54+
Boolean,
55+
}
56+
57+
// eslint-disable-next-line
58+
export type Any = String | Int | Boolean | ID | Float | SchemaCustomScalars[keyof SchemaCustomScalars]
59+
60+
declare global {
61+
interface SchemaCustomScalars {}
62+
}

src/SelectionSet/SelectionSet.test-d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assertType, expectTypeOf, test } from 'vitest'
2-
import type * as Schema from '../../tests/ts/_/schema.js'
2+
import type * as Schema from '../../tests/ts/_/schema/Schema.js'
33
import type { SelectionSet } from './__.js'
44

55
type Q = SelectionSet.Query<Schema.$.Index>
@@ -29,6 +29,13 @@ test(`Query`, () => {
2929
// non-null
3030
assertType<Q>({ idNonNull: true })
3131

32+
// Custom Scalar
33+
assertType<Q>({ date: true })
34+
assertType<Q>({ date: false })
35+
assertType<Q>({ date: 0 })
36+
assertType<Q>({ date: 1 })
37+
assertType<Q>({ date: undefined })
38+
3239
// Enum
3340
assertType<Q>({ abcEnum: true })
3441

src/SelectionSet/toGraphQLDocumentString.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { parse, print } from 'graphql'
22
import { describe, expect, test } from 'vitest'
3-
import type * as Schema from '../../tests/ts/_/schema.js'
3+
import type * as Schema from '../../tests/ts/_/schema/Schema.js'
44
import type { SelectionSet } from './__.js'
55
import { toGraphQLDocumentString } from './toGraphQLDocumentString.js'
66

src/cli/generate.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const args = Command.create().description(`Generate a type safe GraphQL client.`
99
.parameter(`schema`, z.string().min(1).describe(`File path to where your GraphQL schema is.`))
1010
.parameter(
1111
`output`,
12-
z.string().min(1).optional().describe(
13-
`File path for where to output the generated TypeScript types. If not given, outputs to stdout.`,
12+
z.string().min(1).describe(
13+
`Directory path for where to output the generated TypeScript files.`,
1414
),
1515
)
1616
.settings({
@@ -23,8 +23,5 @@ const args = Command.create().description(`Generate a type safe GraphQL client.`
2323
const schemaSource = await fs.readFile(args.schema, `utf8`)
2424
const code = generateCode({ schemaSource })
2525

26-
if (args.output) {
27-
await fs.writeFile(args.output, code, { encoding: `utf8` })
28-
} else {
29-
console.log(code)
30-
}
26+
await fs.writeFile(`${args.output}/schema.ts`, code.schema, { encoding: `utf8` })
27+
await fs.writeFile(`${args.output}/scalars.ts`, code.scalars, { encoding: `utf8` })

src/client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from 'vitest'
22
import { setupMockServer } from '../tests/raw/__helpers.js'
3-
import type { $ } from '../tests/ts/_/schema.js'
3+
import type { $ } from '../tests/ts/_/schema/Schema.js'
44
import { create } from './client.js'
55

66
const ctx = setupMockServer()

src/entrypoints/alpha/scalars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from '../../Schema/NamedType/Scalar/Scalar.js'

src/generator/generator.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const referenceRenderers = defineReferenceRenderers({
106106
GraphQLInterfaceType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLInterfaceType, node.name),
107107
GraphQLObjectType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLObjectType, node.name),
108108
GraphQLUnionType: (_, node) => Code.propertyAccess(namespaceNames.GraphQLUnionType, node.name),
109-
GraphQLScalarType: (_, node) => `_.Scalar.${node.name}`,
109+
GraphQLScalarType: (_, node) => `$Scalar.${node.name}`,
110110
})
111111

112112
const dispatchToConcreteRenderer = (
@@ -312,18 +312,19 @@ const unwrapNonNull = (
312312
return { node: nodeUnwrapped, nullable }
313313
}
314314

315-
const scalarTypeMap: Record<string, 'string' | 'number' | 'boolean'> = {
316-
ID: `string`,
317-
Int: `number`,
318-
String: `string`,
319-
Float: `number`,
320-
Boolean: `boolean`,
321-
}
315+
// const scalarTypeMap: Record<string, 'string' | 'number' | 'boolean'> = {
316+
// ID: `string`,
317+
// Int: `number`,
318+
// String: `string`,
319+
// Float: `number`,
320+
// Boolean: `boolean`,
321+
// }
322322

323323
// high level
324324

325325
interface Input {
326326
schemaModulePath?: string
327+
scalarsModulePath?: string
327328
schemaSource: string
328329
options?: {
329330
TSDoc?: {
@@ -365,13 +366,16 @@ export const generateCode = (input: Input) => {
365366
(_) => _.name === `Subscription`,
366367
)
367368

368-
let code = ``
369+
let schemaCode = ``
369370

370371
const schemaModulePath = input.schemaModulePath ?? `graphql-client/alpha/schema`
372+
const scalarsModulePath = input.scalarsModulePath ?? `graphql-client/alpha/schema/scalars`
371373

372-
code += `import type * as _ from ${Code.quote(schemaModulePath)}\n\n`
374+
schemaCode += `import type * as _ from ${Code.quote(schemaModulePath)}\n`
375+
schemaCode += `import type * as $Scalar from './Scalar.ts'\n`
376+
schemaCode += `\n\n`
373377

374-
code += Code.export$(
378+
schemaCode += Code.export$(
375379
Code.namespace(
376380
`$`,
377381
Code.group(
@@ -409,30 +413,21 @@ export const generateCode = (input: Input) => {
409413
},
410414
),
411415
},
412-
scalars: `Scalars`,
413416
}),
414417
),
415418
),
416-
Code.export$(
417-
Code.interface$(
418-
`Scalars`,
419-
Code.objectFromEntries(typeMapByKind.GraphQLScalarType.map((_) => {
420-
// todo strict mode where instead of falling back to "any" we throw an error
421-
const type = scalarTypeMap[_.name] || `string`
422-
return [_.name, type]
423-
})),
424-
),
425-
),
426419
),
427420
),
428421
)
422+
// console.log(typeMapByKind.GraphQLScalarType)
429423

430424
for (const [name, types] of entries(typeMapByKind)) {
431425
if (name === `GraphQLScalarType`) continue
426+
if (name === `GraphQLCustomScalarType`) continue
432427

433428
const namespaceName = name === `GraphQLRootTypes` ? `Root` : namespaceNames[name]
434-
code += Code.commentSectionTitle(namespaceName)
435-
code += Code.export$(
429+
schemaCode += Code.commentSectionTitle(namespaceName)
430+
schemaCode += Code.export$(
436431
Code.namespace(
437432
namespaceName,
438433
types.length === 0
@@ -444,16 +439,48 @@ export const generateCode = (input: Input) => {
444439
)
445440
}
446441

447-
return code
442+
let scalarsCode = ``
443+
444+
scalarsCode += `import type * as Scalar from ${Code.quote(scalarsModulePath)}
445+
446+
declare global {
447+
interface SchemaCustomScalars {
448+
Date: Date
449+
}
450+
}
451+
452+
${
453+
typeMapByKind.GraphQLCustomScalarType
454+
.map((_) => {
455+
return `
456+
export const ${_.name} = Scalar.scalar('${_.name}', Scalar.nativeScalarConstructors.String)
457+
export type ${_.name} = typeof ${_.name}
458+
`
459+
}).join(`\n`)
460+
}
461+
462+
463+
464+
465+
export * from ${Code.quote(scalarsModulePath)}
466+
`
467+
468+
return {
469+
scalars: scalarsCode,
470+
schema: schemaCode,
471+
}
448472
}
449473

450474
export const generateFile = async (params: {
451475
schemaPath: string
452-
typeScriptPath: string
476+
outputDirPath: string
453477
schemaModulePath?: string
478+
scalarsModulePath?: string
454479
}) => {
455480
// todo use @dprint/formatter
456481
const schemaSource = await fs.readFile(params.schemaPath, `utf8`)
457482
const code = generateCode({ schemaSource, ...params })
458-
await fs.writeFile(params.typeScriptPath, code, { encoding: `utf8` })
483+
await fs.mkdir(params.outputDirPath, { recursive: true })
484+
await fs.writeFile(`${params.outputDirPath}/Schema.ts`, code.schema, { encoding: `utf8` })
485+
await fs.writeFile(`${params.outputDirPath}/Scalar.ts`, code.scalars, { encoding: `utf8` })
459486
}

0 commit comments

Comments
 (0)