Skip to content

fix(internal): fall back to schema parsing for Prisma 7 ModelName#1752

Closed
bellcoTech wants to merge 3 commits into
cedarjs:mainfrom
bellcoTech:fix-codegen-prisma-client-modelname-fallback
Closed

fix(internal): fall back to schema parsing for Prisma 7 ModelName#1752
bellcoTech wants to merge 3 commits into
cedarjs:mainfrom
bellcoTech:fix-codegen-prisma-client-modelname-fallback

Conversation

@bellcoTech
Copy link
Copy Markdown
Contributor

Problem

After migrating a project to Prisma 7's new prisma-client generator provider, yarn cedar generate types (or any command that triggers it, e.g. yarn cedar build, yarn cedar dev) runs without an explicit error but emits broken GraphQL resolver types. yarn cedar type-check then surfaces hundreds of errors like:

Type 'MakeRelationsOptional<Model, never>' is not assignable to ...
Property 'createdAt' does not exist on type 'unknown'.
Property 'id' does not exist on type 'unknown'.

Inspecting the generated api/types/graphql.d.ts shows that the mappers block is empty for every model — every resolver is typed against unknown, and MakeRelationsOptional<Model, never> collapses every relation field to never.

For a real project this can mean 300+ type errors appearing in one go, all on a working codebase, simply because the Prisma generator was switched to the recommended new provider.

Root cause

@cedarjs/internal's codegen builds its GraphQL mappers config by reading Prisma.ModelName from the generated client:

function getModelName(mod: unknown): Record<string, string> | null {
  // …
  const prismaModule = mod.Prisma
  // …
  const modelName = prismaModule.ModelName  // ← gone in Prisma 7's new provider
  // …
}

Prisma 7's new prisma-client provider — the recommended one going forward, see https://www.prisma.io/docs/orm/prisma-schema/overview/generators — deliberately stops exporting Prisma.ModelName as part of the runtime client. The legacy prisma-client-js provider still does, which is why projects on the old provider are unaffected.

With ModelName missing, getPrismaClient() falls through to:

return { ModelName: {} }

…codegen then runs with an empty mappers map and emits the broken resolver types the user sees. There's no error or warning — the broken output looks superficially the same as a working one until type-check is run downstream.

Fix

Add a third fallback that reads model declarations straight from the Prisma schema file (or schema directory — Prisma 7 supports multiple *.prisma files under prisma/schema/). Model declarations have stable syntax across every provider, so a small regex recovers the names without depending on any generated artefact:

async function readModelNamesFromSchema(): Promise<Record<string, string> | null> {
  try {
    const cedarPaths = getPaths()
    const schemaPath = await getSchemaPath(cedarPaths.api.prismaConfig)
    if (!schemaPath || !fs.existsSync(schemaPath)) {
      return null
    }

    const schemaSource = fs.statSync(schemaPath).isDirectory()
      ? fs.readdirSync(schemaPath)
          .filter((entry) => entry.endsWith('.prisma'))
          .map((entry) => fs.readFileSync(path.join(schemaPath, entry), 'utf8'))
          .join('\n')
      : fs.readFileSync(schemaPath, 'utf8')

    const modelRegex = /^\s*model\s+(\w+)\s*\{/gm
    const modelNames: Record<string, string> = {}
    let match
    while ((match = modelRegex.exec(schemaSource)) !== null) {
      modelNames[match[1]] = match[1]
    }

    return Object.keys(modelNames).length > 0 ? modelNames : null
  } catch {
    return null
  }
}

…wired in after both importGeneratedPrismaClient() attempts:

// Prisma 7's `prisma-client` provider no longer exports `Prisma.ModelName`,
// so neither attempt above can populate it. Fall back to parsing the schema
// file(s) directly — model declarations are stable across all providers.
const schemaModelNames = await readModelNamesFromSchema()
if (schemaModelNames) {
  return { ModelName: schemaModelNames }
}

return { ModelName: {} }

The fallback only runs when both client-import attempts succeed-but-without-ModelName (or both throw). Projects on the legacy prisma-client-js provider take exactly the same path they always have — getModelName(localPrisma) returns the real record on the first attempt and we return early — so there is no behavioural change for existing setups.

The regex is the same one Prisma's own parser uses for the model declaration head; it doesn't try to understand model bodies (we only need names), and a malformed schema simply yields an empty set, returning null and letting the existing empty-mapper path run.

Checks

Per CONTRIBUTING.md:

  • yarn check — clean (no constraint or dedupe issues)
  • yarn test in packages/internal — 164 passing, including the existing graphqlCodeGen.test.ts suite
  • yarn build in packages/internal — clean (ESM + CJS)
  • yarn lint — clean (via pre-commit hook)

No new dependencies (getSchemaPath is already exported from @cedarjs/project-config, which @cedarjs/internal already depends on). Verified end-to-end on a real Prisma 7 project: codegen now emits correct mappers and yarn cedar type-check passes against the same codebase that was previously producing 300+ errors.

Prisma 7's new `prisma-client` provider no longer exports
`Prisma.ModelName`, which `getPrismaClient()` relies on to populate the
codegen mappers config. When both import attempts succeed but yield no
ModelName record, codegen ends up with an empty mappers map and emits
broken type definitions (e.g. `type X = MakeRelationsOptional<Y, never>`),
producing hundreds of downstream type errors in user projects.

Add a third fallback that reads model declarations directly from the
Prisma schema file (or schema directory, supported in Prisma 7). Model
declarations have stable syntax across all providers, so a small regex
recovers the names without depending on any generated artefacts.

Wired in after both `importGeneratedPrismaClient()` attempts; behaviour
is unchanged for projects on the legacy `prisma-client-js` provider
because the first attempt still resolves `Prisma.ModelName` normally.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 11, 2026

👷 Deploy request for cedarjs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 340bb32

@github-actions github-actions Bot added this to the next-release-patch milestone May 11, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR fixes a silent codegen regression for projects using Prisma 7's new prisma-client generator provider, which stopped exporting Prisma.ModelName at runtime. Without this export, getPrismaClient() fell through to { ModelName: {} }, causing every GraphQL resolver to be typed against unknown with no visible error.

  • Adds readModelNamesFromSchema() as a third fallback in getPrismaClient() that parses model declarations directly from the Prisma schema file(s), including support for Prisma 7's multi-file schema directories.
  • Exports the function for direct testability; includes two new test files: a unit-level suite for readModelNamesFromSchema (8 cases covering single-file, multi-file directory, subdirectory regression, missing schema, etc.) and an end-to-end regression test that verifies codegen output contains correct mappers when the Prisma client omits ModelName.
  • The docs in docs/implementation-docs/2026-03-26-cedarjs-project-overview.md remain factually accurate — Prisma 7 is already the listed runtime version, and the internal package description is unchanged.

Confidence Score: 4/5

Safe to merge with one latency issue for Prisma 7 users that was flagged in a prior review.

The fix is functionally correct and well-tested. The only outstanding concern — already documented in a prior outside-diff comment — is that a Prisma 7 project with an already-generated client hits the unnecessary execa.sync(prisma generate) shell-out on every cedar generate types invocation before reaching the schema fallback, because the first importGeneratedPrismaClient() succeeds without throwing, leaving getModelName returning null and the code falling through into the regeneration branch. This adds the full prisma generate latency to every type-gen invocation for that generator provider.

The control flow in getPrismaClient() around the execa.sync call in graphqlCodeGen.ts is worth a second look.

Important Files Changed

Filename Overview
packages/internal/src/generate/graphqlCodeGen.ts Adds readModelNamesFromSchema() fallback with correct withFileTypes+isFile() guard. The schema-parsing fallback is only reached after an unnecessary prisma generate shell-out for Prisma 7 users whose client is already generated (noted in prior review comments).
packages/internal/src/tests/readModelNamesFromSchema.test.ts Comprehensive unit tests for the new fallback: single-file schema, multi-file directory, subdirectory-named-.prisma regression, no-models, missing path, thrown getSchemaPath, and regex edge cases — all exercised against real fs via a temp directory.
packages/internal/src/tests/graphqlCodeGen.prismaClientProvider.test.ts End-to-end regression test verifying that codegen emits correct mappers when the mocked Prisma client deliberately omits ModelName. Uses frozen system time to correlate the dynamic import cache-bust URL with the pre-registered module mock.

Reviews (3): Last reviewed commit: "test(internal): end-to-end codegen test ..." | Re-trigger Greptile

Comment thread packages/internal/src/generate/graphqlCodeGen.ts
Comment thread packages/internal/src/generate/graphqlCodeGen.ts Outdated
Addresses feedback on cedarjs#1752:

- `readdirSync` without `withFileTypes` doesn't distinguish files from
  subdirectories, so a directory named e.g. `views.prisma/` would slip
  through the `.endsWith('.prisma')` filter and cause
  `readFileSync(directoryPath)` to throw `EISDIR`. The outer try/catch
  would swallow that and the fallback would silently return `null`.
  Switch to `withFileTypes: true` + `entry.isFile()`.

- Add `packages/internal/src/__tests__/readModelNamesFromSchema.test.ts`
  with dedicated coverage for the fallback. The function silently
  swallows all errors and returns `null` on failure, so a regression
  would reproduce the very symptom the PR fixes (empty mappers, type
  errors) without any visible failure signal — explicit unit tests
  prevent that. Cases covered:

  - single-file schema parsing
  - multi-file directory schema (only `.prisma` files merged)
  - subdirectories ending in `.prisma` are ignored (greptile-bot case)
  - schema with no model declarations returns null
  - getSchemaPath resolving to null returns null
  - non-existent schema path returns null
  - getSchemaPath throwing returns null
  - regex matches indented, tight, and padded model declarations and
    ignores commented-out ones

Tests use a real temp directory rather than memfs so the `isFile()`
filter exercises real filesystem semantics, and to avoid adding memfs
as a new devDependency.

`readModelNamesFromSchema` is exported solely to make these tests
possible — the public-API surface of `@cedarjs/internal` is unchanged
in practice because consumers import from the package root, not from
this submodule.
@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

Ughh :(
Typegen from Prisma to GraphQL could really use some major TLC...

It'd be great with a unit tests to catch regressions. Can you help with that?

EDIT: Lol. I see you did just add unit tests! Thank you! 🙏

@bellcoTech
Copy link
Copy Markdown
Contributor Author

@Tobbe
The second commit (1ad96a8) added unit tests for the new function — flagging in case that crossed your push.

If you want a regression test at the codegen-output level too (full pipeline against a Prisma 7-shaped fixture), happy to add that. Or is what's there enough?

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 11, 2026

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 340bb32

Command Status Duration Result
nx run-many -t build:pack --exclude create-ceda... ✅ Succeeded 2s View ↗
nx run-many -t build ✅ Succeeded 4s View ↗
nx run-many -t test --minWorkers=1 --maxWorkers=4 ✅ Succeeded 1m 46s View ↗
nx run-many -t test:types ✅ Succeeded 11s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-11 14:12:14 UTC

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

@bellcoTech It bugs me I didn't catch this even once for all the hundreds of times I've spun up our local testing project(s). Also, why isn't yarn cedar type-check catching this?! Or any of the yarn cedar test runs we do on the test project.

Could you please take a look at adding something to __fixtures__/test-project that verifies that the types comes through as expected?

Addresses follow-up review on
cedarjs#1752 asking for a regression
test "at the codegen-output level" — one that would have caught the
broken-mappers symptom before it shipped, not just the helper in
isolation.

Adds `graphqlCodeGen.prismaClientProvider.test.ts`:

- Reuses the `example-todo-main` fixture's real `schema.prisma`
  (model `Todo`).
- Mocks the Prisma module to deliberately omit `Prisma.ModelName` —
  the exact shape Prisma 7's `prisma-client` provider produces at
  runtime, which is what triggered the original silent-empty-mappers
  bug.
- Stubs `execa.sync` so the intermediate `cedar prisma generate`
  shell-out in `getPrismaClient` is a no-op.
- Stubs `getSchemaPath` to point at the fixture's schema (the fixture
  predates Prisma's `prisma.config.{ts,cjs}` style, so the real
  `loadPrismaConfig` would throw on the missing config file — that's
  a separate piece of plumbing not under test here).
- Runs `generateTypeDefGraphQLApi` end-to-end and asserts on the
  generated `graphql.d.ts` content:
  - `import { Todo as PrismaTodo } from 'src/lib/db'` is present —
    proves the schema-parsing fallback ran and recovered the model
    name.
  - `type AllMappedModels = MaybeOrArrayOfMaybe<Todo>` is present —
    `printMappedModelsPlugin`'s output, the strongest single signal
    that mappers came through.
  - `type AllMappedModels = MaybeOrArrayOfMaybe<>` is _not_ present —
    negative assertion against regressing back to empty mappers.

Together with the existing helper-level tests in
`readModelNamesFromSchema.test.ts`, this gives two layers of cover for
the regression class: the unit tests pin the helper's behaviour, and
this test pins the codegen pipeline's behaviour when the helper is
the only thing providing model names.
@bellcoTech
Copy link
Copy Markdown
Contributor Author

@Tobbe I've added some tests I'll keep working on it.
I had a bit of a dig

  1. check-test-project-fixture.yml runs yarn cedar build --no-prerender. Build succeeds even when mappers come through empty — broken types only show up at type-check time.
  2. api/types/graphql.d.ts and web/types/graphql.d.ts are in the fixture's .gitignore, so the workflow's git status diff can't see the broken file even though it's regenerated every run.
  3. No workflow runs yarn cedar type-check against any fixture (closest is yarn cedar test api/web, but vitest doesn't type-check by default).
    So the breakage produces a wrong graphql.d.ts silently on every PR and nothing trips.

The new tests should pick this up as it does an end to end with assertion

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

@bellcoTech We do run type-check here https://github.com/cedarjs/cedar/blob/main/.github/workflows/cli-smoke-tests.yml#L121 But maybe there isn't anything to typecheck.

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

I also did consider suggesting adding vitest typechecks (https://v3.vitest.dev/guide/testing-types.html), but only the ESM fixture would be able to run them. So it's a bit more complicated to set up. But definitely doable

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

I created a new Cedar app, and just ran a scaffold for the included UserExample model, and types seems to come through for me

image

Do I need more involved types for things to break?

It uses Prisma's new prisma-client provider
image

@bellcoTech
Copy link
Copy Markdown
Contributor Author

@Tobbe what does the top of api/types/graphql.d.ts look like? From memory I had import { } caused by the empty mappers, so the types fall back to the sdl types. Which is fine but my project has heaps of relations, hand written service files and sdl-prisma divergence so the sdl types dont match prisma which is where it fell down.

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

import { Prisma } from "src/lib/db"
import { MergePrismaWithSdlTypes, MakeRelationsOptional } from '@cedarjs/api'
import { UserExample as PrismaUserExample } from 'src/lib/db'
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
import { CedarGraphQLContext } from '@cedarjs/graphql-server/dist/types';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
export type ResolverFn<TResult, TParent, TContext, TArgs> = (
      args?: TArgs,
      obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo }
    ) => TResult | Promise<TResult>
export type RequireFields<T, K extends keyof T> = Omit<T, K> & { [P in K]-?: NonNullable<T[P]> };
export type OptArgsResolverFn<TResult, TParent = {}, TContext = {}, TArgs = {}> = (
      args?: TArgs,
      obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo }
    ) => TResult | Promise<TResult>

    export type RequiredResolverFn<TResult, TParent = {}, TContext = {}, TArgs = {}> = (
      args: TArgs,
      obj: { root: TParent; context: TContext; info: GraphQLResolveInfo }
    ) => TResult | Promise<TResult>
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }

If I look at the Prisma type, I see this:

export const ModelName = {
  UserExample: 'UserExample'
} as const

export type ModelName = (typeof ModelName)[keyof typeof ModelName]

@bellcoTech
Copy link
Copy Markdown
Contributor Author

bellcoTech commented May 11, 2026

Ah ok I can reproduce it in a new project now. When I did the upgrade I kept the api as cjs, so I changed

  • generatedFileExtension = "ts"
  • importFileExtension = "js"
    in the prisma schema so the generated client fit the rest of the project's TS conventions. Problem is cedar's codegen does a raw Node import() on that file at runtime, and Node can't load a .ts file inside a "type": "commonjs" package when its content is ESM-style imports — so the import throws, gets swallowed, and the type generator ends up with empty mappers. Everything downstream falls over from there.
    Assuming you want to keep cjs compatibility working for now, happy to reshape the PR around that — or it might be worth a doc note somewhere so the next person doesn't trip on it the same way as I know the plan is to go full esm in the future.

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 11, 2026

@bellcoTech Ahh, yeah, I had to play around a lot with TS config settings and prisma settings to get it all to play nice together for both CJS and ESM.

I agree it's a bit weird to have TS files like that mixed with how the rest of your app looks, but it's the only way I could get it to work.

So what Cedar generates for a new app by default is CJS, and it works with the prisma settings you get by default.
But yeah, now that you mention it, I think I got a report from someone else too about this. So definitely worth a comment somewhere. What do you think about a comment in schema.prisma right above the generator client section?

@bellcoTech
Copy link
Copy Markdown
Contributor Author

Yeah I think a comment in the schema file is fine. That way at least you're not playing around with this when trying to get it all to behave. Let me make changes on my project to make sure there's nothing else lingering when I change it too

@bellcoTech
Copy link
Copy Markdown
Contributor Author

@Tobbe Confirming now that I have everything else working together, changing back from ts was not breaking. I think a comment in schema is fine. Happy for me to leave that with you so you can do in your commenting style? This PR can be closed

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 12, 2026

Awesome. Thanks for confirming.

So the two real issues you found were the lock race condition in the CLI + the auth headers stuff for request handlers?

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 12, 2026

I'll follow up with a separate PR with a schema file comment. Leaving this PR open as a reminder in the meantime

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 12, 2026

Closing this in favor of #1761

@bellcoTech I'm still interested in your answers to the question I posted above regarding the two "real" issues :)

@Tobbe Tobbe closed this May 12, 2026
Tobbe added a commit that referenced this pull request May 12, 2026
Had this comment first, but it felt like it was too long

```js
// For CJS Cedar apps:
// It might feel a little out of place to have .mts files in the codebase, and
// especially importing them like we do in `src/lib/db`.
// For ESM Cedar apps:
// It might feel a little weird to ask Prisma to generate CJS compatible format.
//
// Setting `moduleFormat = "cjs"` and using `.mts` extensions is however the
// only combination that makes Prisma 7, which is ESM-only, work in CJS Cedar
// apps while also having the same configuration work for ESM Cedar apps.
//
// The only reason this works is thanks to Node 24's support for require(esm).
// https://www.prisma.io/docs/orm/prisma-schema/overview/generators
```
So I shortened it to what we have now, and then pointing here for more
info.

This all came out of the discussion on PR
#1752
@bellcoTech
Copy link
Copy Markdown
Contributor Author

@Tobbe Apologies I've been off. Correct re the other two. I'm still fighting local when I change back my prisma file. All works on prod though so I assume I've stuffed something up. I'll look at it more when I'm back tomorrow

@Tobbe
Copy link
Copy Markdown
Member

Tobbe commented May 12, 2026

@bellcoTech No need to apologize. Thanks for your PRs and collaboration 🙏

Tobbe added a commit that referenced this pull request May 13, 2026
Had this comment first, but it felt like it was too long

```js
// For CJS Cedar apps:
// It might feel a little out of place to have .mts files in the codebase, and
// especially importing them like we do in `src/lib/db`.
// For ESM Cedar apps:
// It might feel a little weird to ask Prisma to generate CJS compatible format.
//
// Setting `moduleFormat = "cjs"` and using `.mts` extensions is however the
// only combination that makes Prisma 7, which is ESM-only, work in CJS Cedar
// apps while also having the same configuration work for ESM Cedar apps.
//
// The only reason this works is thanks to Node 24's support for require(esm).
// https://www.prisma.io/docs/orm/prisma-schema/overview/generators
```
So I shortened it to what we have now, and then pointing here for more
info.

This all came out of the discussion on PR
#1752
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants