-
-
Notifications
You must be signed in to change notification settings - Fork 25
fix(internal): fall back to schema parsing for Prisma 7 ModelName #1752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
bellcoTech
wants to merge
3
commits into
cedarjs:main
from
bellcoTech:fix-codegen-prisma-client-modelname-fallback
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
8ba5ca1
fix(internal): fall back to schema parsing for Prisma 7 ModelName
bellcoTech 1ad96a8
review: filter subdirs in readModelNamesFromSchema + add unit tests
bellcoTech 340bb32
test(internal): end-to-end codegen test for Prisma 7 ModelName fallback
bellcoTech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
139 changes: 139 additions & 0 deletions
139
packages/internal/src/__tests__/graphqlCodeGen.prismaClientProvider.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| /** | ||
| * End-to-end regression test for the Prisma 7 `prisma-client` generator | ||
| * provider scenario. | ||
| * | ||
| * Prisma 7's new `prisma-client` provider deliberately stops exporting | ||
| * `Prisma.ModelName` at runtime, which is what `@cedarjs/internal`'s codegen | ||
| * previously relied on to populate its GraphQL `mappers` config. Without that | ||
| * record, codegen used to emit a `graphql.d.ts` with empty mappers — every | ||
| * resolver typed against `unknown`, every relation collapsed to `never` — | ||
| * which only surfaced as hundreds of type errors when the project ran | ||
| * `cedar type-check` downstream. | ||
| * | ||
| * This file exercises the actual codegen output (not just the helper in | ||
| * isolation) so a future regression in the schema-parsing fallback would | ||
| * fail this test rather than silently producing broken types. | ||
| */ | ||
| import fs from 'node:fs' | ||
| import path from 'node:path' | ||
|
|
||
| const { mockPrismaClientPath, mockPrismaClientFileUrl } = await vi.hoisted( | ||
| async () => { | ||
| const p = await import('node:path') | ||
| const { pathToFileURL } = await import('node:url') | ||
|
|
||
| const mockPrismaClientPath = p.resolve('/mock-prisma-client-path') | ||
| const mockPrismaClientFileUrl = pathToFileURL(mockPrismaClientPath) | ||
| return { mockPrismaClientPath, mockPrismaClientFileUrl } | ||
| }, | ||
| ) | ||
|
|
||
| import { afterAll, afterEach, beforeAll, expect, test, vi } from 'vitest' | ||
|
|
||
| import type * as ProjectConfig from '@cedarjs/project-config' | ||
|
|
||
| import { generateTypeDefGraphQLApi } from '../generate/graphqlCodeGen.js' | ||
| import { generateGraphQLSchema } from '../generate/graphqlSchema.js' | ||
|
|
||
| const FIXTURE_PATH = path.resolve( | ||
| __dirname, | ||
| '../../../../__fixtures__/example-todo-main', | ||
| ) | ||
|
|
||
| beforeAll(() => { | ||
| process.env.CEDAR_CWD = FIXTURE_PATH | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| delete process.env.CEDAR_CWD | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks() | ||
| }) | ||
|
|
||
| vi.mock('@cedarjs/project-config', async (importOriginal) => { | ||
| const original = await importOriginal<typeof ProjectConfig>() | ||
| const p = await import('node:path') | ||
|
|
||
| return { | ||
| ...original, | ||
| resolveGeneratedPrismaClient: () => | ||
| Promise.resolve({ clientPath: mockPrismaClientPath, error: undefined }), | ||
| // The `example-todo-main` fixture predates Prisma's `prisma.config.{ts,cjs}` | ||
| // style, so `loadPrismaConfig` (called by the real `getSchemaPath`) would | ||
| // throw on the missing config file. Short-circuit it to the fixture's | ||
| // actual schema path — the rest of the schema-fallback pipeline still | ||
| // runs against real `fs` against a real schema file. | ||
| getSchemaPath: () => | ||
| Promise.resolve( | ||
| p.resolve( | ||
| __dirname, | ||
| '../../../../__fixtures__/example-todo-main/api/prisma/schema.prisma', | ||
| ), | ||
| ), | ||
| } | ||
| }) | ||
|
|
||
| // Stub `execa` so that the intermediate `cedar prisma generate` shell-out in | ||
| // `getPrismaClient` is a no-op — we don't want this test to actually invoke | ||
| // the CLI. | ||
| vi.mock('execa', () => { | ||
| return { | ||
| default: { | ||
| sync: vi.fn(), | ||
| }, | ||
| } | ||
| }) | ||
|
|
||
| const mockNow = vi.hoisted(() => new Date().getTime()) | ||
|
|
||
| // Crucially, the mocked Prisma client *omits* `ModelName`. This is what the | ||
| // Prisma 7 `prisma-client` generator produces at runtime and is the exact | ||
| // shape that previously caused codegen to emit empty mappers. | ||
| vi.mock(mockPrismaClientFileUrl + '?t=' + mockNow, () => { | ||
| return { | ||
| Prisma: { | ||
| // intentionally no `ModelName` | ||
| }, | ||
| } | ||
| }) | ||
|
|
||
| test('codegen recovers ModelName from schema when Prisma client omits it', async () => { | ||
| await generateGraphQLSchema() | ||
|
|
||
| vi.setSystemTime(mockNow) | ||
|
|
||
| let codegenOutput: { | ||
| file: fs.PathOrFileDescriptor | ||
| data: string | ArrayBufferView | ||
| } = { file: '', data: '' } | ||
|
|
||
| vi.spyOn(fs, 'writeFileSync').mockImplementation( | ||
| (file: fs.PathOrFileDescriptor, data: string | ArrayBufferView) => { | ||
| codegenOutput = { file, data } | ||
| }, | ||
| ) | ||
|
|
||
| const { typeDefFiles, errors } = await generateTypeDefGraphQLApi() | ||
| expect(errors).toEqual([]) | ||
| expect(typeDefFiles).toHaveLength(1) | ||
|
|
||
| const data = String(codegenOutput.data) | ||
|
|
||
| // The fixture's `schema.prisma` declares a single `Todo` model. With the | ||
| // schema-parsing fallback wired in, the codegen output should import it | ||
| // from prisma and use it in the resolver mapper aliases — even though the | ||
| // mocked Prisma client doesn't expose `ModelName`. | ||
| expect(data).toContain("import { Todo as PrismaTodo } from 'src/lib/db'") | ||
|
|
||
| // `AllMappedModels` is generated by `printMappedModelsPlugin` and is the | ||
| // strongest single signal that mappers came through. An empty mapper map | ||
| // produces `MaybeOrArrayOfMaybe<>` here; a working one produces | ||
| // `MaybeOrArrayOfMaybe<Todo>`. | ||
| expect(data).toContain('type AllMappedModels = MaybeOrArrayOfMaybe<Todo>') | ||
|
|
||
| // And the negative assertion — make sure we don't regress back to the | ||
| // empty-mapper output. | ||
| expect(data).not.toContain('type AllMappedModels = MaybeOrArrayOfMaybe<>') | ||
| }) |
184 changes: 184 additions & 0 deletions
184
packages/internal/src/__tests__/readModelNamesFromSchema.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import fs from 'node:fs' | ||
| import os from 'node:os' | ||
| import path from 'node:path' | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' | ||
|
|
||
| import type * as ProjectConfig from '@cedarjs/project-config' | ||
|
|
||
| // `getPaths` / `getSchemaPath` are the only project-config helpers exercised by | ||
| // `readModelNamesFromSchema`. Stub them to point at a per-test temp directory | ||
| // so the function under test does real filesystem work against fixtures we | ||
| // control, without dragging in the rest of a Cedar project. | ||
| const mockGetPaths = vi.hoisted(() => | ||
| vi.fn<() => { api: { prismaConfig: string | null } }>(), | ||
| ) | ||
| const mockGetSchemaPath = vi.hoisted(() => | ||
| vi.fn<(prismaConfig: string | null) => Promise<string | null>>(), | ||
| ) | ||
|
|
||
| vi.mock('@cedarjs/project-config', async (importOriginal) => { | ||
| const original = await importOriginal<typeof ProjectConfig>() | ||
| return { | ||
| ...original, | ||
| getPaths: mockGetPaths, | ||
| getSchemaPath: mockGetSchemaPath, | ||
| } | ||
| }) | ||
|
|
||
| // Import after the mocks are registered so `readModelNamesFromSchema` picks | ||
| // them up. | ||
| const { readModelNamesFromSchema } = | ||
| await import('../generate/graphqlCodeGen.js') | ||
|
|
||
| let tmpDir: string | ||
|
|
||
| beforeEach(() => { | ||
| tmpDir = fs.mkdtempSync( | ||
| path.join(os.tmpdir(), 'cedar-read-model-names-from-schema-'), | ||
| ) | ||
| mockGetPaths.mockReturnValue({ api: { prismaConfig: null } }) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }) | ||
| vi.clearAllMocks() | ||
| }) | ||
|
|
||
| describe('readModelNamesFromSchema', () => { | ||
| it('parses model declarations from a single-file schema', async () => { | ||
| const schemaPath = path.join(tmpDir, 'schema.prisma') | ||
| fs.writeFileSync( | ||
| schemaPath, | ||
| [ | ||
| 'generator client { provider = "prisma-client" }', | ||
| '', | ||
| 'model User {', | ||
| ' id Int @id', | ||
| '}', | ||
| '', | ||
| 'model Post {', | ||
| ' id Int @id', | ||
| ' authorId Int', | ||
| '}', | ||
| '', | ||
| ].join('\n'), | ||
| ) | ||
| mockGetSchemaPath.mockResolvedValue(schemaPath) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toEqual({ User: 'User', Post: 'Post' }) | ||
| }) | ||
|
|
||
| it('merges model declarations across multi-file directory schemas', async () => { | ||
| const schemaDir = path.join(tmpDir, 'schema') | ||
| fs.mkdirSync(schemaDir) | ||
| fs.writeFileSync( | ||
| path.join(schemaDir, 'user.prisma'), | ||
| ['model User {', ' id Int @id', '}'].join('\n'), | ||
| ) | ||
| fs.writeFileSync( | ||
| path.join(schemaDir, 'post.prisma'), | ||
| ['model Post {', ' id Int @id', '}'].join('\n'), | ||
| ) | ||
| // Non-prisma file in the same dir should be ignored. | ||
| fs.writeFileSync( | ||
| path.join(schemaDir, 'notes.txt'), | ||
| 'model Decoy { id Int @id }', | ||
| ) | ||
| mockGetSchemaPath.mockResolvedValue(schemaDir) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toEqual({ User: 'User', Post: 'Post' }) | ||
| }) | ||
|
|
||
| it('ignores subdirectories whose names end with .prisma', async () => { | ||
| // Regression: a previous version filtered by name only and crashed with | ||
| // EISDIR when a subdirectory happened to be named e.g. `views.prisma/`. | ||
| const schemaDir = path.join(tmpDir, 'schema') | ||
| fs.mkdirSync(schemaDir) | ||
| fs.mkdirSync(path.join(schemaDir, 'views.prisma')) | ||
| fs.writeFileSync( | ||
| path.join(schemaDir, 'main.prisma'), | ||
| ['model User {', ' id Int @id', '}'].join('\n'), | ||
| ) | ||
| mockGetSchemaPath.mockResolvedValue(schemaDir) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toEqual({ User: 'User' }) | ||
| }) | ||
|
|
||
| it('returns null when the schema has no model declarations', async () => { | ||
| const schemaPath = path.join(tmpDir, 'schema.prisma') | ||
| fs.writeFileSync( | ||
| schemaPath, | ||
| [ | ||
| 'generator client { provider = "prisma-client" }', | ||
| 'datasource db { provider = "postgresql" url = env("DATABASE_URL") }', | ||
| ].join('\n'), | ||
| ) | ||
| mockGetSchemaPath.mockResolvedValue(schemaPath) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null when getSchemaPath resolves to null', async () => { | ||
| mockGetSchemaPath.mockResolvedValue(null) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null when the schema path does not exist', async () => { | ||
| mockGetSchemaPath.mockResolvedValue(path.join(tmpDir, 'missing.prisma')) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| it('returns null when getSchemaPath throws', async () => { | ||
| mockGetSchemaPath.mockRejectedValue(new Error('boom')) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('regex matches `model` lines with surrounding whitespace and ignores comments', async () => { | ||
| const schemaPath = path.join(tmpDir, 'schema.prisma') | ||
| fs.writeFileSync( | ||
| schemaPath, | ||
| [ | ||
| '// model CommentedOut { id Int @id }', | ||
| ' model Indented {', | ||
| ' id Int @id', | ||
| '}', | ||
| '', | ||
| 'model Tight{', | ||
| ' id Int @id', | ||
| '}', | ||
| '', | ||
| 'model PaddedName {', | ||
| ' id Int @id', | ||
| '}', | ||
| ].join('\n'), | ||
| ) | ||
| mockGetSchemaPath.mockResolvedValue(schemaPath) | ||
|
|
||
| const result = await readModelNamesFromSchema() | ||
|
|
||
| expect(result).toEqual({ | ||
| Indented: 'Indented', | ||
| Tight: 'Tight', | ||
| PaddedName: 'PaddedName', | ||
| }) | ||
| expect(result).not.toHaveProperty('CommentedOut') | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.