From a940c497a39e4285f89b4573c3cbd382a930f2b7 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 27 Sep 2024 16:39:00 -0400 Subject: [PATCH] fix(generator): support any schema file name (#1143) --- src/cli/generate.ts | 4 +- .../__snapshots__/config.test.ts.snap | 98 +++++++++++++++++++ src/layers/4_generator/config.test.ts | 28 ++++++ src/layers/4_generator/config.ts | 42 ++++---- src/layers/4_generator/helpers/fs.ts | 5 + tests/_/fixtures/custom.graphql | 3 + tests/_/fixtures/schema.graphql | 3 + tests/_/helpers.ts | 8 ++ 8 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 src/layers/4_generator/__snapshots__/config.test.ts.snap create mode 100644 src/layers/4_generator/config.test.ts create mode 100644 tests/_/fixtures/custom.graphql create mode 100644 tests/_/fixtures/schema.graphql diff --git a/src/cli/generate.ts b/src/cli/generate.ts index cc68cf7d3..a159979bd 100755 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -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( @@ -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: { diff --git a/src/layers/4_generator/__snapshots__/config.test.ts.snap b/src/layers/4_generator/__snapshots__/config.test.ts.snap new file mode 100644 index 000000000..e77f034ee --- /dev/null +++ b/src/layers/4_generator/__snapshots__/config.test.ts.snap @@ -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 +} +" +`; diff --git a/src/layers/4_generator/config.test.ts b/src/layers/4_generator/config.test.ts new file mode 100644 index 000000000..ce2c8d4b8 --- /dev/null +++ b/src/layers/4_generator/config.test.ts @@ -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() +}) diff --git a/src/layers/4_generator/config.ts b/src/layers/4_generator/config.ts index 45923f770..5733c8bc9 100644 --- a/src/layers/4_generator/config.ts +++ b/src/layers/4_generator/config.ts @@ -5,7 +5,7 @@ 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: { @@ -13,7 +13,7 @@ export interface Input { /** * Defaults to the source directory if set, otherwise the current working directory. */ - dirPath?: string + dirOrFilePath?: string } | { type: 'url' url: URL @@ -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 @@ -56,14 +55,15 @@ 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 @@ -71,6 +71,7 @@ export interface Config { } } schema: { + sdl: string instance: GraphQLSchema typeMapByKind: TypeMapByKind error: { @@ -129,18 +130,12 @@ export const createConfig = async (input: Input): Promise => { 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: { @@ -151,6 +146,7 @@ export const createConfig = async (input: Input): Promise => { }, inputs: { root: inputPathDirRoot, + schema: sourceSchema.type === `introspection` ? null : sourceSchema.path, customScalarCodecs: inputPathCustomScalarCodecs, }, }, @@ -163,6 +159,7 @@ export const createConfig = async (input: Input): Promise => { }, }, schema: { + sdl: sourceSchema.content, instance: schema, typeMapByKind, error: { @@ -182,16 +179,21 @@ export const createConfig = async (input: Input): Promise => { } } -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 } } } diff --git a/src/layers/4_generator/helpers/fs.ts b/src/layers/4_generator/helpers/fs.ts index 518886565..3956efe83 100644 --- a/src/layers/4_generator/helpers/fs.ts +++ b/src/layers/4_generator/helpers/fs.ts @@ -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() +} diff --git a/tests/_/fixtures/custom.graphql b/tests/_/fixtures/custom.graphql new file mode 100644 index 000000000..7fed59189 --- /dev/null +++ b/tests/_/fixtures/custom.graphql @@ -0,0 +1,3 @@ +type Query { + customNamedSchemaFile: Boolean +} diff --git a/tests/_/fixtures/schema.graphql b/tests/_/fixtures/schema.graphql new file mode 100644 index 000000000..2827e61bf --- /dev/null +++ b/tests/_/fixtures/schema.graphql @@ -0,0 +1,3 @@ +type Query { + defaultNamedSchemaFile: Boolean +} diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index 83436e05b..0df2824a0 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -4,6 +4,8 @@ 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 } }) @@ -11,6 +13,7 @@ export const createResponse = (body: object) => interface Fixtures { fetch: Mock<(request: Request) => Promise> graffle: Client<{ config: Config; schemaIndex: null }> + pokemonService: SchemaService } export const test = testBase.extend({ @@ -28,4 +31,9 @@ export const test = testBase.extend({ // @ts-expect-error fixme await use(graffle) }, + pokemonService: async ({}, use) => { // eslint-disable-line + const pokemonService = await serveSchema({ schema }) + await use(pokemonService) + await pokemonService.stop() + }, })