Skip to content

Commit a940c49

Browse files
authored
fix(generator): support any schema file name (#1143)
1 parent 670bb46 commit a940c49

File tree

8 files changed

+169
-22
lines changed

8 files changed

+169
-22
lines changed

src/cli/generate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const args = Command.create().description(`Generate a type safe GraphQL client.`
1616
.parameter(
1717
`schema`,
1818
z.string().min(1).describe(
19-
`Path to where your GraphQL schema is. If a URL is given it will be introspected. Otherwise assumed to be a file path to your GraphQL SDL file.`,
19+
`Path to where your GraphQL schema is. If a URL is given it will be introspected. Otherwise assumed to be a path to your GraphQL SDL file. If a directory path is given, then will look for a "schema.graphql" within that directory. Otherwise will attempt to load the exact file path given.`,
2020
),
2121
)
2222
.parametersExclusive(
@@ -92,7 +92,7 @@ const defaultSchemaUrl = typeof args.defaultSchemaUrl === `string`
9292
await Generator.generate({
9393
sourceSchema: url
9494
? { type: `url`, url }
95-
: { type: `sdl`, dirPath: Path.dirname(args.schema) },
95+
: { type: `sdl`, dirOrFilePath: Path.dirname(args.schema) },
9696
defaultSchemaUrl,
9797
name: args.name,
9898
libraryPaths: {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`can introspect schema from url 1`] = `
4+
"interface Being {
5+
id: Int
6+
name: String
7+
}
8+
9+
input DateFilter {
10+
gte: Float
11+
lte: Float
12+
}
13+
14+
type Mutation {
15+
addPokemon(attack: Int, defense: Int, hp: Int, name: String!, type: PokemonType!): Pokemon
16+
}
17+
18+
type Patron implements Being {
19+
id: Int
20+
money: Int
21+
name: String
22+
}
23+
24+
type Pokemon implements Being {
25+
attack: Int
26+
birthday: Int
27+
defense: Int
28+
hp: Int
29+
id: Int
30+
name: String
31+
trainer: Trainer
32+
type: PokemonType
33+
}
34+
35+
input PokemonFilter {
36+
birthday: DateFilter
37+
name: StringFilter
38+
}
39+
40+
enum PokemonType {
41+
electric
42+
fire
43+
grass
44+
water
45+
}
46+
47+
type Query {
48+
beings: [Being!]!
49+
pokemon: [Pokemon!]
50+
pokemonByName(name: String!): [Pokemon!]
51+
pokemons(filter: PokemonFilter): [Pokemon!]
52+
trainerByName(name: String!): Trainer
53+
trainers: [Trainer!]
54+
}
55+
56+
input StringFilter {
57+
contains: String
58+
in: [String!]
59+
}
60+
61+
type Trainer implements Being {
62+
class: TrainerClass
63+
fans: [Patron!]
64+
id: Int
65+
name: String
66+
pokemon: [Pokemon!]
67+
}
68+
69+
enum TrainerClass {
70+
bugCatcher
71+
camper
72+
picnicker
73+
psychic
74+
psychicMedium
75+
psychicYoungster
76+
sailor
77+
superNerd
78+
tamer
79+
teamRocketGrunt
80+
triathlete
81+
youngster
82+
youth
83+
}"
84+
`;
85+
86+
exports[`can load schema from custom dir using default file name 1`] = `
87+
"type Query {
88+
defaultNamedSchemaFile: Boolean
89+
}
90+
"
91+
`;
92+
93+
exports[`can load schema from custom path 1`] = `
94+
"type Query {
95+
customNamedSchemaFile: Boolean
96+
}
97+
"
98+
`;

src/layers/4_generator/config.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import _ from 'json-bigint'
2+
import { expect } from 'vitest'
3+
import { test } from '../../../tests/_/helpers.js'
4+
import { createConfig } from './config.js'
5+
6+
test(`can load schema from custom path`, async () => {
7+
const customPathFile = `./tests/_/fixtures/custom.graphql`
8+
const config = await createConfig({ sourceSchema: { type: `sdl`, dirOrFilePath: customPathFile } })
9+
const field = config.schema.instance.getQueryType()?.getFields()[`customNamedSchemaFile`]
10+
expect(config.paths.project.inputs.schema).toEqual(customPathFile)
11+
expect(config.schema.sdl).toMatchSnapshot()
12+
expect(field).toBeDefined()
13+
})
14+
15+
test(`can load schema from custom dir using default file name`, async () => {
16+
const customPathDir = `tests/_/fixtures`
17+
const config = await createConfig({ sourceSchema: { type: `sdl`, dirOrFilePath: customPathDir } })
18+
const field = config.schema.instance.getQueryType()?.getFields()[`defaultNamedSchemaFile`]
19+
expect(config.paths.project.inputs.schema).toEqual(customPathDir + `/schema.graphql`)
20+
expect(config.schema.sdl).toMatchSnapshot()
21+
expect(field).toBeDefined()
22+
})
23+
24+
test(`can introspect schema from url`, async ({ pokemonService }) => {
25+
const config = await createConfig({ sourceSchema: { type: `url`, url: pokemonService.url } })
26+
expect(config.paths.project.inputs.schema).toEqual(null)
27+
expect(config.schema.sdl).toMatchSnapshot()
28+
})

src/layers/4_generator/config.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import * as Path from 'node:path'
55
import { introspectionQuery } from '../../cli/_helpers.js'
66
import { getTypeMapByKind, type TypeMapByKind } from '../../lib/graphql.js'
77
import { omitUndefinedKeys } from '../../lib/prelude.js'
8-
import { fileExists } from './helpers/fs.js'
8+
import { fileExists, isPathToADirectory } from './helpers/fs.js'
99

1010
export interface Input {
1111
sourceSchema: {
1212
type: 'sdl'
1313
/**
1414
* Defaults to the source directory if set, otherwise the current working directory.
1515
*/
16-
dirPath?: string
16+
dirOrFilePath?: string
1717
} | {
1818
type: 'url'
1919
url: URL
@@ -27,7 +27,6 @@ export interface Input {
2727
*/
2828
sourceDirPath?: string
2929
sourceCustomScalarCodecsFilePath?: string
30-
schemaPath?: string
3130
/**
3231
* Override import paths to graffle package within the generated code.
3332
* Used by Graffle test suite to have generated clients point to source
@@ -56,21 +55,23 @@ export interface Config {
5655
name: string
5756
paths: {
5857
project: {
59-
outputs: {
60-
root: string
61-
modules: string
62-
}
6358
inputs: {
6459
root: string
60+
schema: null | string
6561
customScalarCodecs: string
6662
}
63+
outputs: {
64+
root: string
65+
modules: string
66+
}
6767
}
6868
imports: {
6969
customScalarCodecs: string
7070
grafflePackage: Required<InputLibraryPaths>
7171
}
7272
}
7373
schema: {
74+
sdl: string
7475
instance: GraphQLSchema
7576
typeMapByKind: TypeMapByKind
7677
error: {
@@ -129,18 +130,12 @@ export const createConfig = async (input: Input): Promise<Config> => {
129130

130131
const sourceSchema = await resolveSourceSchema(input)
131132

132-
const schema = buildSchema(sourceSchema)
133+
const schema = buildSchema(sourceSchema.content)
133134
const typeMapByKind = getTypeMapByKind(schema)
134135
const errorObjects = errorTypeNamePattern
135136
? Object.values(typeMapByKind.GraphQLObjectType).filter(_ => _.name.match(errorTypeNamePattern))
136137
: []
137138

138-
// const rootTypes = {
139-
// Query: typeMapByKind.GraphQLRootType.find(_ => _.name === `Query`) ?? null,
140-
// Mutation: typeMapByKind.GraphQLRootType.find(_ => _.name === `Mutation`) ?? null,
141-
// Subscription: typeMapByKind.GraphQLRootType.find(_ => _.name === `Subscription`) ?? null,
142-
// }
143-
144139
return {
145140
name: input.name ?? defaultName,
146141
paths: {
@@ -151,6 +146,7 @@ export const createConfig = async (input: Input): Promise<Config> => {
151146
},
152147
inputs: {
153148
root: inputPathDirRoot,
149+
schema: sourceSchema.type === `introspection` ? null : sourceSchema.path,
154150
customScalarCodecs: inputPathCustomScalarCodecs,
155151
},
156152
},
@@ -163,6 +159,7 @@ export const createConfig = async (input: Input): Promise<Config> => {
163159
},
164160
},
165161
schema: {
162+
sdl: sourceSchema.content,
166163
instance: schema,
167164
typeMapByKind,
168165
error: {
@@ -182,16 +179,21 @@ export const createConfig = async (input: Input): Promise<Config> => {
182179
}
183180
}
184181

185-
const resolveSourceSchema = async (input: Input) => {
182+
const defaultSchemaFileName = `schema.graphql`
183+
184+
const resolveSourceSchema = async (
185+
input: Input,
186+
): Promise<{ type: 'introspection'; content: string } | { type: 'file'; content: string; path: string }> => {
186187
if (input.sourceSchema.type === `sdl`) {
187-
const sourceDirPath = input.sourceSchema.dirPath ?? input.sourceDirPath ?? process.cwd()
188-
const schemaPath = input.schemaPath ?? Path.join(sourceDirPath, `schema.graphql`)
189-
const sdl = await fs.readFile(schemaPath, `utf8`)
190-
return sdl
188+
const fileOrDirPath = input.sourceSchema.dirOrFilePath ?? input.sourceDirPath ?? process.cwd()
189+
const isDir = await isPathToADirectory(fileOrDirPath)
190+
const finalPath = isDir ? Path.join(fileOrDirPath, defaultSchemaFileName) : fileOrDirPath
191+
const sdl = await fs.readFile(finalPath, `utf8`)
192+
return { type: `file`, content: sdl, path: finalPath }
191193
} else {
192194
const data = await introspectionQuery(input.sourceSchema.url)
193195
const schema = buildClientSchema(data)
194196
const sdl = printSchema(schema)
195-
return sdl
197+
return { type: `introspection`, content: sdl }
196198
}
197199
}

src/layers/4_generator/helpers/fs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ export const fileExists = async (path: string) => {
1111
}),
1212
)
1313
}
14+
15+
export const isPathToADirectory = async (path: string) => {
16+
const stat = await fs.stat(path)
17+
return stat.isDirectory()
18+
}

tests/_/fixtures/custom.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Query {
2+
customNamedSchemaFile: Boolean
3+
}

tests/_/fixtures/schema.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Query {
2+
defaultNamedSchemaFile: Boolean
3+
}

tests/_/helpers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import { Graffle } from '../../src/entrypoints/main.js'
44
import type { Config } from '../../src/entrypoints/utilities-for-generated.js'
55
import type { Client } from '../../src/layers/6_client/client.js'
66
import { CONTENT_TYPE_REC } from '../../src/lib/graphqlHTTP.js'
7+
import { type SchemaService, serveSchema } from './lib/serveSchema.js'
8+
import { schema } from './schemas/pokemon/schema.js'
79

810
export const createResponse = (body: object) =>
911
new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': CONTENT_TYPE_REC } })
1012

1113
interface Fixtures {
1214
fetch: Mock<(request: Request) => Promise<Response>>
1315
graffle: Client<{ config: Config; schemaIndex: null }>
16+
pokemonService: SchemaService
1417
}
1518

1619
export const test = testBase.extend<Fixtures>({
@@ -28,4 +31,9 @@ export const test = testBase.extend<Fixtures>({
2831
// @ts-expect-error fixme
2932
await use(graffle)
3033
},
34+
pokemonService: async ({}, use) => { // eslint-disable-line
35+
const pokemonService = await serveSchema({ schema })
36+
await use(pokemonService)
37+
await pokemonService.stop()
38+
},
3139
})

0 commit comments

Comments
 (0)