Skip to content

Commit

Permalink
fix(generator): support any schema file name (#1143)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Sep 27, 2024
1 parent 670bb46 commit a940c49
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 22 deletions.
4 changes: 2 additions & 2 deletions src/cli/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const args = Command.create().description(`Generate a type safe GraphQL client.`
.parameter(
`schema`,
z.string().min(1).describe(
`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.`,
`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.`,
),
)
.parametersExclusive(
Expand Down Expand Up @@ -92,7 +92,7 @@ const defaultSchemaUrl = typeof args.defaultSchemaUrl === `string`
await Generator.generate({
sourceSchema: url
? { type: `url`, url }
: { type: `sdl`, dirPath: Path.dirname(args.schema) },
: { type: `sdl`, dirOrFilePath: Path.dirname(args.schema) },
defaultSchemaUrl,
name: args.name,
libraryPaths: {
Expand Down
98 changes: 98 additions & 0 deletions src/layers/4_generator/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`can introspect schema from url 1`] = `
"interface Being {
id: Int
name: String
}
input DateFilter {
gte: Float
lte: Float
}
type Mutation {
addPokemon(attack: Int, defense: Int, hp: Int, name: String!, type: PokemonType!): Pokemon
}
type Patron implements Being {
id: Int
money: Int
name: String
}
type Pokemon implements Being {
attack: Int
birthday: Int
defense: Int
hp: Int
id: Int
name: String
trainer: Trainer
type: PokemonType
}
input PokemonFilter {
birthday: DateFilter
name: StringFilter
}
enum PokemonType {
electric
fire
grass
water
}
type Query {
beings: [Being!]!
pokemon: [Pokemon!]
pokemonByName(name: String!): [Pokemon!]
pokemons(filter: PokemonFilter): [Pokemon!]
trainerByName(name: String!): Trainer
trainers: [Trainer!]
}
input StringFilter {
contains: String
in: [String!]
}
type Trainer implements Being {
class: TrainerClass
fans: [Patron!]
id: Int
name: String
pokemon: [Pokemon!]
}
enum TrainerClass {
bugCatcher
camper
picnicker
psychic
psychicMedium
psychicYoungster
sailor
superNerd
tamer
teamRocketGrunt
triathlete
youngster
youth
}"
`;
exports[`can load schema from custom dir using default file name 1`] = `
"type Query {
defaultNamedSchemaFile: Boolean
}
"
`;
exports[`can load schema from custom path 1`] = `
"type Query {
customNamedSchemaFile: Boolean
}
"
`;
28 changes: 28 additions & 0 deletions src/layers/4_generator/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import _ from 'json-bigint'
import { expect } from 'vitest'
import { test } from '../../../tests/_/helpers.js'
import { createConfig } from './config.js'

test(`can load schema from custom path`, async () => {
const customPathFile = `./tests/_/fixtures/custom.graphql`
const config = await createConfig({ sourceSchema: { type: `sdl`, dirOrFilePath: customPathFile } })
const field = config.schema.instance.getQueryType()?.getFields()[`customNamedSchemaFile`]
expect(config.paths.project.inputs.schema).toEqual(customPathFile)
expect(config.schema.sdl).toMatchSnapshot()
expect(field).toBeDefined()
})

test(`can load schema from custom dir using default file name`, async () => {
const customPathDir = `tests/_/fixtures`
const config = await createConfig({ sourceSchema: { type: `sdl`, dirOrFilePath: customPathDir } })
const field = config.schema.instance.getQueryType()?.getFields()[`defaultNamedSchemaFile`]
expect(config.paths.project.inputs.schema).toEqual(customPathDir + `/schema.graphql`)
expect(config.schema.sdl).toMatchSnapshot()
expect(field).toBeDefined()
})

test(`can introspect schema from url`, async ({ pokemonService }) => {
const config = await createConfig({ sourceSchema: { type: `url`, url: pokemonService.url } })
expect(config.paths.project.inputs.schema).toEqual(null)
expect(config.schema.sdl).toMatchSnapshot()
})
42 changes: 22 additions & 20 deletions src/layers/4_generator/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import * as Path from 'node:path'
import { introspectionQuery } from '../../cli/_helpers.js'
import { getTypeMapByKind, type TypeMapByKind } from '../../lib/graphql.js'
import { omitUndefinedKeys } from '../../lib/prelude.js'
import { fileExists } from './helpers/fs.js'
import { fileExists, isPathToADirectory } from './helpers/fs.js'

export interface Input {
sourceSchema: {
type: 'sdl'
/**
* Defaults to the source directory if set, otherwise the current working directory.
*/
dirPath?: string
dirOrFilePath?: string
} | {
type: 'url'
url: URL
Expand All @@ -27,7 +27,6 @@ export interface Input {
*/
sourceDirPath?: string
sourceCustomScalarCodecsFilePath?: string
schemaPath?: string
/**
* Override import paths to graffle package within the generated code.
* Used by Graffle test suite to have generated clients point to source
Expand Down Expand Up @@ -56,21 +55,23 @@ export interface Config {
name: string
paths: {
project: {
outputs: {
root: string
modules: string
}
inputs: {
root: string
schema: null | string
customScalarCodecs: string
}
outputs: {
root: string
modules: string
}
}
imports: {
customScalarCodecs: string
grafflePackage: Required<InputLibraryPaths>
}
}
schema: {
sdl: string
instance: GraphQLSchema
typeMapByKind: TypeMapByKind
error: {
Expand Down Expand Up @@ -129,18 +130,12 @@ export const createConfig = async (input: Input): Promise<Config> => {

const sourceSchema = await resolveSourceSchema(input)

const schema = buildSchema(sourceSchema)
const schema = buildSchema(sourceSchema.content)
const typeMapByKind = getTypeMapByKind(schema)
const errorObjects = errorTypeNamePattern
? Object.values(typeMapByKind.GraphQLObjectType).filter(_ => _.name.match(errorTypeNamePattern))
: []

// const rootTypes = {
// Query: typeMapByKind.GraphQLRootType.find(_ => _.name === `Query`) ?? null,
// Mutation: typeMapByKind.GraphQLRootType.find(_ => _.name === `Mutation`) ?? null,
// Subscription: typeMapByKind.GraphQLRootType.find(_ => _.name === `Subscription`) ?? null,
// }

return {
name: input.name ?? defaultName,
paths: {
Expand All @@ -151,6 +146,7 @@ export const createConfig = async (input: Input): Promise<Config> => {
},
inputs: {
root: inputPathDirRoot,
schema: sourceSchema.type === `introspection` ? null : sourceSchema.path,
customScalarCodecs: inputPathCustomScalarCodecs,
},
},
Expand All @@ -163,6 +159,7 @@ export const createConfig = async (input: Input): Promise<Config> => {
},
},
schema: {
sdl: sourceSchema.content,
instance: schema,
typeMapByKind,
error: {
Expand All @@ -182,16 +179,21 @@ export const createConfig = async (input: Input): Promise<Config> => {
}
}

const resolveSourceSchema = async (input: Input) => {
const defaultSchemaFileName = `schema.graphql`

const resolveSourceSchema = async (
input: Input,
): Promise<{ type: 'introspection'; content: string } | { type: 'file'; content: string; path: string }> => {
if (input.sourceSchema.type === `sdl`) {
const sourceDirPath = input.sourceSchema.dirPath ?? input.sourceDirPath ?? process.cwd()
const schemaPath = input.schemaPath ?? Path.join(sourceDirPath, `schema.graphql`)
const sdl = await fs.readFile(schemaPath, `utf8`)
return sdl
const fileOrDirPath = input.sourceSchema.dirOrFilePath ?? input.sourceDirPath ?? process.cwd()
const isDir = await isPathToADirectory(fileOrDirPath)
const finalPath = isDir ? Path.join(fileOrDirPath, defaultSchemaFileName) : fileOrDirPath
const sdl = await fs.readFile(finalPath, `utf8`)
return { type: `file`, content: sdl, path: finalPath }
} else {
const data = await introspectionQuery(input.sourceSchema.url)
const schema = buildClientSchema(data)
const sdl = printSchema(schema)
return sdl
return { type: `introspection`, content: sdl }
}
}
5 changes: 5 additions & 0 deletions src/layers/4_generator/helpers/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export const fileExists = async (path: string) => {
}),
)
}

export const isPathToADirectory = async (path: string) => {
const stat = await fs.stat(path)
return stat.isDirectory()
}
3 changes: 3 additions & 0 deletions tests/_/fixtures/custom.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type Query {
customNamedSchemaFile: Boolean
}
3 changes: 3 additions & 0 deletions tests/_/fixtures/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type Query {
defaultNamedSchemaFile: Boolean
}
8 changes: 8 additions & 0 deletions tests/_/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { Graffle } from '../../src/entrypoints/main.js'
import type { Config } from '../../src/entrypoints/utilities-for-generated.js'
import type { Client } from '../../src/layers/6_client/client.js'
import { CONTENT_TYPE_REC } from '../../src/lib/graphqlHTTP.js'
import { type SchemaService, serveSchema } from './lib/serveSchema.js'
import { schema } from './schemas/pokemon/schema.js'

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

interface Fixtures {
fetch: Mock<(request: Request) => Promise<Response>>
graffle: Client<{ config: Config; schemaIndex: null }>
pokemonService: SchemaService
}

export const test = testBase.extend<Fixtures>({
Expand All @@ -28,4 +31,9 @@ export const test = testBase.extend<Fixtures>({
// @ts-expect-error fixme
await use(graffle)
},
pokemonService: async ({}, use) => { // eslint-disable-line
const pokemonService = await serveSchema({ schema })
await use(pokemonService)
await pokemonService.stop()
},
})

0 comments on commit a940c49

Please sign in to comment.