From 8cf50dae3d866c2479743e7bc9af3f27ac05c68d Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 11:54:52 +0100 Subject: [PATCH 1/4] Adds running the puzzmo codebase inside the tests in order to figure out whats slow --- src/index.ts | 15 ++++------ src/sharedSchema.ts | 8 +++-- src/tests/integration.puzzmo.test.ts | 45 ++++++++++++++++++++++++++++ src/utils.ts | 8 +++++ 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/tests/integration.puzzmo.test.ts diff --git a/src/index.ts b/src/index.ts index 4d618f4..36c3e4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,8 @@ export * from "./types.js" import { basename, join } from "node:path" +import { makeStep } from "./utils.js" + export interface SDLCodeGenReturn { // Optional way to start up a watcher mode for the codegen createWatcher: () => { fileChanged: (path: string) => Promise } @@ -22,7 +24,10 @@ export interface SDLCodeGenReturn { } /** The API specifically for the Redwood preset */ -export async function runFullCodegen(preset: "redwood", config: { paths: RedwoodPaths; verbose?: true }): Promise +export async function runFullCodegen( + preset: "redwood", + config: { paths: RedwoodPaths; sys?: typescript.System; verbose?: true } +): Promise export async function runFullCodegen(preset: string, config: unknown): Promise @@ -149,11 +154,3 @@ const isRedwoodServiceFile = (file: string) => { if (file.endsWith("scenarios.ts") || file.endsWith("scenarios.js")) return false return file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") } - -const makeStep = (verbose: boolean) => async (msg: string, fn: () => Promise | Promise | void) => { - if (!verbose) return fn() - console.log("[sdl-codegen] " + msg) - console.time("[sdl-codegen] " + msg) - await fn() - console.timeEnd("[sdl-codegen] " + msg) -} diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 1821a5a..4c9ef3f 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -6,10 +6,14 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" import { typeMapper } from "./typeMap.js" +import { makeStep } from "./utils.js" export const createSharedSchemaFiles = async (context: AppContext) => { - await createSharedExternalSchemaFile(context) - await createSharedReturnPositionSchemaFile(context) + const verbose = !!(context as { verbose?: true }).verbose + const step = makeStep(verbose) + + await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) + await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename), diff --git a/src/tests/integration.puzzmo.test.ts b/src/tests/integration.puzzmo.test.ts new file mode 100644 index 0000000..0bcf91c --- /dev/null +++ b/src/tests/integration.puzzmo.test.ts @@ -0,0 +1,45 @@ +import { existsSync } from "node:fs" +import { join, resolve } from "node:path" + +import { createSystem } from "@typescript/vfs" +import { describe, expect, it } from "vitest" + +import { runFullCodegen } from "../index" + +it("Passes", () => expect(true).toBe(true)) + +const hasAccessToPuzzmo = existsSync("../app/package.json") +const desc = hasAccessToPuzzmo ? describe : describe.skip + +desc("Puzzmo", () => { + it("Runs the entire puzzmo codebase fast", async () => { + const puzzmoAPIWD = resolve(process.cwd() + "/..../../../app/apps/api.puzzmo.com") + const vfsMap = new Map() + const vfs = createSystem(vfsMap) + + // Replicates a Redwood project config object + const paths = { + base: puzzmoAPIWD, + api: { + base: puzzmoAPIWD, + config: "-", + dbSchema: join(puzzmoAPIWD, "prisma", "schema.prisma"), + directives: join(puzzmoAPIWD, "src", "directives"), + graphql: join(puzzmoAPIWD, "src", "functions", "graphql.ts"), + lib: join(puzzmoAPIWD, "src", "lib"), + models: "-", + services: join(puzzmoAPIWD, "src", "services"), + src: join(puzzmoAPIWD, "src"), + types: join(puzzmoAPIWD, "types"), + }, + generated: { + schema: join(puzzmoAPIWD, "..", "..", "api-schema.graphql"), + }, + web: {}, + scripts: "-", + } + + const results = await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) + console.log(results) + }) +}) diff --git a/src/utils.ts b/src/utils.ts index cff11aa..695d0d1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -56,3 +56,11 @@ export const createAndReferOrInlineArgsForField = ( return `${config.name}Args` } + +export const makeStep = (verbose: boolean) => async (msg: string, fn: () => Promise | Promise | void) => { + if (!verbose) return fn() + console.log("[sdl-codegen] " + msg) + console.time("[sdl-codegen] " + msg) + await fn() + console.timeEnd("[sdl-codegen] " + msg) +} From a1213c9ceee79536425f39aee7272a5b6f026d1e Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 13:37:12 +0100 Subject: [PATCH 2/4] Gets the generated shared files faster --- src/sharedSchema.ts | 47 ++++++++++++------- .../features/generatesTypesForUnions.test.ts | 6 +-- .../supportRefferingToEnumsOnlyInSDL.test.ts | 4 +- src/tests/integration.puzzmo.test.ts | 3 +- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 4c9ef3f..322e19d 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -31,6 +31,9 @@ async function createSharedExternalSchemaFile(context: AppContext) { const externalTSFile = context.tsProject.createSourceFile(`/source/${context.pathSettings.sharedFilename}`, "") + const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] + const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -54,7 +57,8 @@ async function createSharedExternalSchemaFile(context: AppContext) { docs.push(type.description) } - externalTSFile.addInterface({ + interfaces.push({ + kind: tsMorph.StructureKind.Interface, name: type.name, isExported: true, docs: [], @@ -91,9 +95,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: '"' + type @@ -105,9 +110,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: type .getTypes() .map((m) => m.name) @@ -116,15 +122,11 @@ async function createSharedExternalSchemaFile(context: AppContext) { } }) + externalTSFile.addInterfaces(interfaces) + externalTSFile.addTypeAliases(typeAliases) + const { scalars } = mapper.getReferencedGraphQLThingsInMapping() - if (scalars.length) { - externalTSFile.addTypeAliases( - scalars.map((s) => ({ - name: s, - type: "any", - })) - ) - } + if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) const formatted = await formatDTS(fullPath, externalTSFile.getText()) @@ -157,6 +159,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { ` ) + const interfaces = [] as tsMorph.InterfaceDeclarationStructure[] + const typeAliases = [] as tsMorph.TypeAliasDeclarationStructure[] + Object.keys(types).forEach((name) => { if (name.startsWith("__")) { return @@ -177,8 +182,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { return } - externalTSFile.addInterface({ + interfaces.push({ name: type.name, + kind: tsMorph.StructureKind.Interface, isExported: true, docs: [], properties: [ @@ -204,9 +210,10 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isEnumType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, isExported: true, + kind: tsMorph.StructureKind.TypeAlias, type: '"' + type @@ -218,8 +225,9 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { } if (graphql.isUnionType(type)) { - externalTSFile.addTypeAlias({ + typeAliases.push({ name: type.name, + kind: tsMorph.StructureKind.TypeAlias, isExported: true, type: type .getTypes() @@ -247,15 +255,18 @@ async function createSharedReturnPositionSchemaFile(context: AppContext) { namedImports: allPrismaModels.map((p) => `${p} as P${p}`), }) - allPrismaModels.forEach((p) => { - externalTSFile.addTypeAlias({ + externalTSFile.addTypeAliases( + allPrismaModels.map((p) => ({ isExported: true, name: p, type: `P${p}`, - }) - }) + })) + ) } + externalTSFile.addInterfaces(interfaces) + externalTSFile.addTypeAliases(typeAliases) + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) const formatted = await formatDTS(fullPath, externalTSFile.getText()) diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 6125ea5..07c2ef8 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -48,8 +48,6 @@ export const Game = { id: number; } - export type Gameish = Game | Puzzle; - export interface Query { __typename?: \\"Query\\"; gameObj?: Game | null | Puzzle | null | null; @@ -59,6 +57,8 @@ export const Game = { export interface Mutation { __typename?: \\"Mutation\\"; __?: string | null; - }" + } + + export type Gameish = Game | Puzzle;" `) }) diff --git a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts index 54dcdb9..75975b3 100644 --- a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts @@ -76,12 +76,12 @@ export const Game: GameResolvers = {}; allGames: Game[]; } - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; - export interface Mutation { __typename?: \\"Mutation\\"; __?: string | null; } + + export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; " `) }) diff --git a/src/tests/integration.puzzmo.test.ts b/src/tests/integration.puzzmo.test.ts index 0bcf91c..5a4a71a 100644 --- a/src/tests/integration.puzzmo.test.ts +++ b/src/tests/integration.puzzmo.test.ts @@ -39,7 +39,6 @@ desc("Puzzmo", () => { scripts: "-", } - const results = await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) - console.log(results) + await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) }) }) From 3fb669b19871f574c843143cb6dd09f8eacc87fd Mon Sep 17 00:00:00 2001 From: orta Date: Sun, 15 Sep 2024 15:14:23 +0100 Subject: [PATCH 3/4] Use the typescript factory ap --- src/formatDTS.ts | 3 +- src/index.ts | 6 +- src/serviceFile.ts | 3 +- src/sharedSchema.ts | 14 +- src/sharedSchemaTSC.ts | 134 ++++++++++++++++++ .../features/generatesTypesForUnions.test.ts | 26 ++-- .../supportRefferingToEnumsOnlyInSDL.test.ts | 19 ++- 7 files changed, 170 insertions(+), 35 deletions(-) create mode 100644 src/sharedSchemaTSC.ts diff --git a/src/formatDTS.ts b/src/formatDTS.ts index 3adf0ca..a8863d1 100644 --- a/src/formatDTS.ts +++ b/src/formatDTS.ts @@ -1,15 +1,16 @@ // https://prettier.io/docs/en/api.html let hasPrettierInstalled = false +let prettier = null try { hasPrettierInstalled = !!require.resolve("prettier") + prettier = require("prettier") } catch (error) {} export const formatDTS = async (path: string, content: string): Promise => { if (!hasPrettierInstalled) return content try { - const prettier = await import("prettier") if (!prettier) return content return prettier.format(content, { filepath: path }) } catch (error) { diff --git a/src/index.ts b/src/index.ts index 36c3e4b..ae08066 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,15 +89,15 @@ export async function runFullCodegen(preset: string, config: unknown): Promise { - const sharedDTSes = await createSharedSchemaFiles(appContext) + const sharedDTSes = await createSharedSchemaFiles(appContext, verbose) filepaths.push(...sharedDTSes) }) let knownServiceFiles: string[] = [] const createDTSFilesForAllServices = async () => { - // TODO: Maybe Redwood has an API for this? Its grabbing all the services const serviceFiles = appContext.sys.readDirectory(appContext.pathSettings.apiServicesPath) knownServiceFiles = serviceFiles.filter(isRedwoodServiceFile) + for (const path of knownServiceFiles) { const dts = await lookAtServiceFile(path, appContext) if (dts) filepaths.push(dts) @@ -123,7 +123,7 @@ export async function runFullCodegen(preset: string, config: unknown): Promise getGraphQLSDLFromFile(appContext.pathSettings)) - await step("Create all shared schema files", () => createSharedSchemaFiles(appContext)) + await step("Create all shared schema files", () => createSharedSchemaFiles(appContext, verbose)) await step("Create all service files", createDTSFilesForAllServices) } else if (path === appContext.pathSettings.prismaDSLPath) { await step("Prisma schema changed", () => getPrismaSchemaFromFile(appContext.pathSettings)) diff --git a/src/serviceFile.ts b/src/serviceFile.ts index f3718ae..44958c2 100644 --- a/src/serviceFile.ts +++ b/src/serviceFile.ts @@ -28,7 +28,6 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const mutationType = gql.getMutationType()! - if (!mutationType) throw new Error("No mutation type") const externalMapper = typeMapper(context, { preferPrismaModels: true }) @@ -201,6 +200,8 @@ export const lookAtServiceFile = async (file: string, context: AppContext) => { ], returnType, }) + + interfaceDeclaration.forget() } /** Ideally, we want to be able to write the type for just the object */ diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index 322e19d..dd5559e 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -5,14 +5,15 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" +import { createSharedExternalSchemaFileViaTSC } from "./sharedSchemaTSC.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" -export const createSharedSchemaFiles = async (context: AppContext) => { - const verbose = !!(context as { verbose?: true }).verbose +export const createSharedSchemaFiles = async (context: AppContext, verbose: boolean) => { const step = makeStep(verbose) await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) + await step("Creating shared schema files via tsc", () => createSharedExternalSchemaFileViaTSC(context)) await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ @@ -122,8 +123,13 @@ async function createSharedExternalSchemaFile(context: AppContext) { } }) - externalTSFile.addInterfaces(interfaces) - externalTSFile.addTypeAliases(typeAliases) + context.tsProject.forgetNodesCreatedInBlock(() => { + externalTSFile.addInterfaces(interfaces) + }) + + context.tsProject.forgetNodesCreatedInBlock(() => { + externalTSFile.addTypeAliases(typeAliases) + }) const { scalars } = mapper.getReferencedGraphQLThingsInMapping() if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) diff --git a/src/sharedSchemaTSC.ts b/src/sharedSchemaTSC.ts new file mode 100644 index 0000000..a3340d1 --- /dev/null +++ b/src/sharedSchemaTSC.ts @@ -0,0 +1,134 @@ +/// The main schema for objects and inputs + +import * as graphql from "graphql" +import ts from "typescript" + +import { AppContext } from "./context.js" +import { formatDTS } from "./formatDTS.js" +import { typeMapper } from "./typeMap.js" + +export async function createSharedExternalSchemaFileViaTSC(context: AppContext) { + const gql = context.gql + const types = gql.getTypeMap() + const knownPrimitives = ["String", "Boolean", "Int"] + + const { prisma, fieldFacts } = context + const mapper = typeMapper(context, {}) + + const statements = [] as ts.Statement[] + + console.time("") + + Object.keys(types).forEach((name) => { + if (name.startsWith("__")) { + return + } + + if (knownPrimitives.includes(name)) { + return + } + + const type = types[name] + const pType = prisma.get(name) + + if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { + // This is slower than it could be, use the add many at once api + const docs = [] + if (pType?.leadingComments) { + docs.push(pType.leadingComments) + } + + if (type.description) { + docs.push(type.description) + } + + const properties = [ + ts.factory.createPropertySignature( + undefined, + "__typename", + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.name)) + ), + ] + + Object.entries(type.getFields()).forEach(([fieldName, obj]: [string, graphql.GraphQLField]) => { + const docs = [] + const prismaField = pType?.properties.get(fieldName) + const type = obj.type as graphql.GraphQLType + + if (prismaField?.leadingComments.length) { + docs.push(prismaField.leadingComments.trim()) + } + + // if (obj.description) docs.push(obj.description); + const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation + const isOptionalInSDL = !graphql.isNonNullType(type) + const doesNotExistInPrisma = false // !prismaField; + + const hasQuestionToken = hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma) + const mappedType = mapper.map(type, { preferNullOverUndefined: true }) + if (mappedType) { + properties.push( + ts.factory.createPropertySignature( + undefined, + fieldName, + hasQuestionToken ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, + ts.factory.createTypeReferenceNode(mappedType) + ) + ) + } + }) + + const interfaceD = ts.factory.createInterfaceDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(name), + undefined, + undefined, + properties + ) + + statements.push(interfaceD) + } + + if (graphql.isEnumType(type)) { + const values = type.getValues().map((m) => (m as { value: string }).value) + const typeKind = `"${values.join('" | "')}"` + + statements.push(ts.factory.createTypeAliasDeclaration(undefined, type.name, [], ts.factory.createTypeReferenceNode(typeKind))) + } + + if (graphql.isUnionType(type)) { + const types = type.getTypes().map((t) => t.name) + const typeKind = types.join(" | ") + statements.push( + ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + type.name, + [], + ts.factory.createTypeReferenceNode(typeKind) + ) + ) + } + }) + + const { scalars } = mapper.getReferencedGraphQLThingsInMapping() + if (scalars.length) { + statements.push( + ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + "Scalars", + [], + ts.factory.createTypeReferenceNode(`{ ${scalars.join(", ")} }`) + ) + ) + } + + const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None) + const printer = ts.createPrinter({}) + const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, sourceFile) + + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) + + const prior = context.sys.readFile(fullPath) + if (prior !== result) context.sys.writeFile(fullPath, result) +} diff --git a/src/tests/features/generatesTypesForUnions.test.ts b/src/tests/features/generatesTypesForUnions.test.ts index 07c2ef8..a6845be 100644 --- a/src/tests/features/generatesTypesForUnions.test.ts +++ b/src/tests/features/generatesTypesForUnions.test.ts @@ -39,26 +39,22 @@ export const Game = { const dts = vfsMap.get("/types/shared-schema-types.d.ts")! expect(dts.trim()).toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id?: number; + __typename?: \\"Game\\"; + id?: number; } - export interface Puzzle { - __typename?: \\"Puzzle\\"; - id: number; + __typename?: \\"Puzzle\\"; + id: number; } - + export type Gameish = Game | Puzzle; export interface Query { - __typename?: \\"Query\\"; - gameObj?: Game | null | Puzzle | null | null; - gameArr: (Game | Puzzle)[]; + __typename?: \\"Query\\"; + gameObj?: Game| null | Puzzle| null| null; + gameArr: (Game | Puzzle)[]; } - export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string | null; - } - - export type Gameish = Game | Puzzle;" + __typename?: \\"Mutation\\"; + __?: string| null; + }" `) }) diff --git a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts index 75975b3..2796bb9 100644 --- a/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts +++ b/src/tests/features/supportRefferingToEnumsOnlyInSDL.test.ts @@ -66,22 +66,19 @@ export const Game: GameResolvers = {}; expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` "export interface Game { - __typename?: \\"Game\\"; - id: number; - games: Game[]; + __typename?: \\"Game\\"; + id: number; + games: Game[]; } - export interface Query { - __typename?: \\"Query\\"; - allGames: Game[]; + __typename?: \\"Query\\"; + allGames: Game[]; } - + type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; export interface Mutation { - __typename?: \\"Mutation\\"; - __?: string | null; + __typename?: \\"Mutation\\"; + __?: string| null; } - - export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; " `) }) From 24c50a22dd241b700a4e089ec2c913d475df4d61 Mon Sep 17 00:00:00 2001 From: Orta Therox Date: Tue, 29 Oct 2024 12:39:24 +0000 Subject: [PATCH 4/4] USe structures api --- src/sharedSchema.ts | 9 +- src/sharedSchemaStructures.ts | 154 ++++++++++++++++++++++++++++++++++ src/sharedSchemaTSC.ts | 2 +- 3 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/sharedSchemaStructures.ts diff --git a/src/sharedSchema.ts b/src/sharedSchema.ts index dd5559e..da4e649 100644 --- a/src/sharedSchema.ts +++ b/src/sharedSchema.ts @@ -5,6 +5,7 @@ import * as tsMorph from "ts-morph" import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" +import { createSharedExternalSchemaFileViaStructure } from "./sharedSchemaStructures.js" import { createSharedExternalSchemaFileViaTSC } from "./sharedSchemaTSC.js" import { typeMapper } from "./typeMap.js" import { makeStep } from "./utils.js" @@ -14,6 +15,8 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) await step("Creating shared schema files via tsc", () => createSharedExternalSchemaFileViaTSC(context)) + await step("Creating shared schema files via structure", () => createSharedExternalSchemaFileViaStructure(context)) + await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) return [ @@ -22,7 +25,7 @@ export const createSharedSchemaFiles = async (context: AppContext, verbose: bool ] } -async function createSharedExternalSchemaFile(context: AppContext) { +function createSharedExternalSchemaFile(context: AppContext) { const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"] @@ -135,10 +138,10 @@ async function createSharedExternalSchemaFile(context: AppContext) { if (scalars.length) externalTSFile.addTypeAliases(scalars.map((s) => ({ name: s, type: "any" }))) const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) - const formatted = await formatDTS(fullPath, externalTSFile.getText()) + const text = externalTSFile.getText() const prior = context.sys.readFile(fullPath) - if (prior !== formatted) context.sys.writeFile(fullPath, formatted) + if (prior !== text) context.sys.writeFile(fullPath, text) } async function createSharedReturnPositionSchemaFile(context: AppContext) { diff --git a/src/sharedSchemaStructures.ts b/src/sharedSchemaStructures.ts new file mode 100644 index 0000000..74fcf67 --- /dev/null +++ b/src/sharedSchemaStructures.ts @@ -0,0 +1,154 @@ +/// The main schema for objects and inputs + +import * as graphql from "graphql" +import * as tsMorph from "ts-morph" + +import { AppContext } from "./context.js" +import { typeMapper } from "./typeMap.js" + +export function createSharedExternalSchemaFileViaStructure(context: AppContext) { + const { gql, prisma, fieldFacts } = context + const types = gql.getTypeMap() + const mapper = typeMapper(context, { preferPrismaModels: true }) + + const typesToImport = [] as string[] + const knownPrimitives = ["String", "Boolean", "Int"] + + const externalTSFile = context.tsProject.createSourceFile( + `/source/a/${context.pathSettings.sharedInternalFilename}`, + ` +// You may very reasonably ask yourself, 'what is this file?' and why do I need it. + +// Roughly, this file ensures that when a resolver wants to return a type - that +// type will match a prisma model. This is useful because you can trivially extend +// the type in the SDL and not have to worry about type mis-matches because the thing +// you returned does not include those functions. + +// This gets particularly valuable when you want to return a union type, an interface, +// or a model where the prisma model is nested pretty deeply (GraphQL connections, for example.) + +` + ) + + const statements = [] as tsMorph.StatementStructures[] + + Object.keys(types).forEach((name) => { + if (name.startsWith("__")) { + return + } + + if (knownPrimitives.includes(name)) { + return + } + + const type = types[name] + const pType = prisma.get(name) + + if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { + // Return straight away if we have a matching type in the prisma schema + // as we dont need it + if (pType) { + typesToImport.push(name) + return + } + + statements.push({ + name: type.name, + kind: tsMorph.StructureKind.Interface, + isExported: true, + docs: [], + properties: [ + { + name: "__typename", + type: `"${type.name}"`, + hasQuestionToken: true, + }, + ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { + const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation + const isOptionalInSDL = !graphql.isNonNullType(obj.type) + const doesNotExistInPrisma = false // !prismaField; + + const field: tsMorph.OptionalKind = { + name: fieldName, + type: mapper.map(obj.type, { preferNullOverUndefined: true }), + hasQuestionToken: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), + } + return field + }), + ], + }) + } + + if (graphql.isEnumType(type)) { + statements.push({ + name: type.name, + isExported: true, + kind: tsMorph.StructureKind.TypeAlias, + type: + '"' + + type + .getValues() + .map((m) => (m as { value: string }).value) + .join('" | "') + + '"', + }) + } + + if (graphql.isUnionType(type)) { + statements.push({ + name: type.name, + kind: tsMorph.StructureKind.TypeAlias, + isExported: true, + type: type + .getTypes() + .map((m) => m.name) + .join(" | "), + }) + } + }) + + const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() + if (scalars.length) { + statements.push( + ...scalars.map( + (s) => + ({ + kind: tsMorph.StructureKind.TypeAlias, + name: s, + type: "any", + } as const) + ) + ) + } + + const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() + if (allPrismaModels.length) { + statements.push({ + kind: tsMorph.StructureKind.ImportDeclaration, + moduleSpecifier: `@prisma/client`, + namedImports: allPrismaModels.map((p) => `${p} as P${p}`), + }) + + statements.push( + ...allPrismaModels.map( + (p) => + ({ + kind: tsMorph.StructureKind.TypeAlias, + name: p, + type: `P${p}`, + } as const) + ) + ) + } + + const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) + externalTSFile.set({ statements }) + const text = externalTSFile.getText() + + // console.log(sourceFileStructure.statements) + // console.log(text) + // const formatted = await formatDTS(fullPath, externalTSFile) + + const prior = context.sys.readFile(fullPath) + if (prior !== text) context.sys.writeFile(fullPath, text) +} diff --git a/src/sharedSchemaTSC.ts b/src/sharedSchemaTSC.ts index a3340d1..e175b40 100644 --- a/src/sharedSchemaTSC.ts +++ b/src/sharedSchemaTSC.ts @@ -7,7 +7,7 @@ import { AppContext } from "./context.js" import { formatDTS } from "./formatDTS.js" import { typeMapper } from "./typeMap.js" -export async function createSharedExternalSchemaFileViaTSC(context: AppContext) { +export function createSharedExternalSchemaFileViaTSC(context: AppContext) { const gql = context.gql const types = gql.getTypeMap() const knownPrimitives = ["String", "Boolean", "Int"]