feat(tml-2892): contract-JSON-driven Migration base + typed contract views#879
Conversation
|
Important Review skippedToo many files! This PR contains 186 files, which is 36 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (186)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughContract-view helpers now promote built-in kinds from default namespaces, add Mongo/SQL/Postgres/SQLite view wrappers, expand fixtures and tests, and update retail-store migrations to read collections and validators from ChangesContract view projections
Sequence Diagram(s)sequenceDiagram
participant MongoContractView_fromJson as MongoContractView.fromJson
participant MongoContractSerializer
participant buildMongoContractView
participant buildSingleNamespaceView
participant buildNamespaceAccessor
participant composeContractView
MongoContractView_fromJson->>MongoContractSerializer: deserializeContract(json)
MongoContractSerializer-->>MongoContractView_fromJson: contract
MongoContractView_fromJson->>buildMongoContractView: wrap(contract)
buildMongoContractView->>buildSingleNamespaceView: default namespace entries
buildMongoContractView->>buildNamespaceAccessor: storage.namespaces
buildMongoContractView->>composeContractView: accessors + namespace map
composeContractView-->>MongoContractView_fromJson: projected view
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-framework/1-core/framework-components/src/ir/contract-view.ts`:
- Around line 47-50: The helper currently returns an unconstrained TView while
accepting builtinKinds as a plain readonly string array, so the returned view
type can promise materialized builtin properties that may actually be undefined.
Update promoteBuiltinKinds and the related helper in contract-view.ts to take
the builtin kinds as a tuple literal type and derive the return type from that
tuple instead of casting to TView; keep the type relationship aligned so callers
can only request view properties that are guaranteed by the provided
builtinKinds.
In `@packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts`:
- Line 2: The import in postgres-contract-view.test-d.ts is currently type-only,
but PostgresContractView is used in a value position via typeof
PostgresContractView.from<Contract>. Change the import to a normal import so the
value binding exists at compile time, and keep the existing usage of
PostgresContractView in the type query unchanged.
In `@packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json`:
- Around line 558-565: The SQLite contract JSON fixture is out of sync with the
paired emitted type fixture, since its sql capabilities are missing the enums
and lateral flags that the sqlite-contract.d.ts fixture still expects.
Regenerate both sqlite-contract.json and sqlite-contract.d.ts from the same
emitted Contract output, or update both fixtures together so the Contract shape
matches in both runtime and type fixtures. Keep the sql.capabilities object
consistent across the sqlite contract fixtures, especially around sql.enums and
sql.lateral.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: b6669857-bd63-4f5f-8ed3-8a07ea5c38e4
📒 Files selected for processing (27)
examples/retail-store/migrations/app/20260513T0505_initial/migration.tsexamples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.tsexamples/retail-store/test/migration.test.tspackages/1-framework/1-core/framework-components/src/exports/ir.tspackages/1-framework/1-core/framework-components/src/ir/contract-view.tspackages/1-framework/1-core/framework-components/test/contract-view.test.tspackages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.tspackages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.tspackages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.tspackages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.tspackages/2-mongo-family/9-family/src/exports/ir.tspackages/2-sql/1-core/contract/package.jsonpackages/2-sql/1-core/contract/src/contract-view.tspackages/2-sql/1-core/contract/src/exports/contract-view.tspackages/2-sql/1-core/contract/tsdown.config.tspackages/3-targets/3-targets/postgres/src/core/postgres-contract-view.tspackages/3-targets/3-targets/postgres/src/exports/runtime.tspackages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.d.tspackages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.jsonpackages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.tspackages/3-targets/3-targets/postgres/test/postgres-contract-view.test.tspackages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.tspackages/3-targets/3-targets/sqlite/src/exports/runtime.tspackages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.d.tspackages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.jsonpackages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.tspackages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts
| export function promoteBuiltinKinds<TView>( | ||
| entries: Readonly<Record<string, unknown>>, | ||
| builtinKinds: readonly string[], | ||
| ): TView { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Tie builtinKinds to the returned view type.
These helpers currently blind-cast to an arbitrary TView, but builtinKinds is just readonly string[]. Because this module is now exported publicly, a caller can request a view type that guarantees valueSet/collection while passing a list that does not include that kind, and the helper will return undefined at runtime for a property the type says is always materialized. Make the built-in kind tuple part of the function signature and derive the return type from it instead of accepting an unconstrained TView.
Proposed direction
-export function promoteBuiltinKinds<TView>(
- entries: Readonly<Record<string, unknown>>,
- builtinKinds: readonly string[],
-): TView {
+export function promoteBuiltinKinds<
+ TEntries extends Readonly<Record<string, unknown>>,
+ const TBuiltinKinds extends readonly string[],
+>(
+ entries: TEntries,
+ builtinKinds: TBuiltinKinds,
+): SingleNamespaceView<TEntries, TBuiltinKinds[number]> {
...
- return blindCast<TView, 'view is built to the SingleNamespaceView shape the caller parametrizes'>(
+ return blindCast<
+ SingleNamespaceView<TEntries, TBuiltinKinds[number]>,
+ 'view is built from entries and builtinKinds'
+ >(
view,
);
}Also applies to: 79-91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/1-framework/1-core/framework-components/src/ir/contract-view.ts`
around lines 47 - 50, The helper currently returns an unconstrained TView while
accepting builtinKinds as a plain readonly string array, so the returned view
type can promise materialized builtin properties that may actually be undefined.
Update promoteBuiltinKinds and the related helper in contract-view.ts to take
the builtin kinds as a tuple literal type and derive the return type from that
tuple instead of casting to TView; keep the type relationship aligned so callers
can only request view properties that are guaranteed by the provided
builtinKinds.
Address review on #879. The view was a class with a private constructor and a static `from()` returning a separate mapped type — a namespace cosplaying as a class, and a narrow projection that was not substitutable for Contract. - Each `<Target>ContractView` is now a plain `{ from, fromJson }` factory; the classes are deleted. `fromJson` deserializes via the serializer and wraps in one step, so a migration declares the view once. - View type is now `Contract & accessors` (a superset) — one value serves as both the contract and the accessor surface. - Collision-safe namespace access: contract envelope fields always win at the root; every namespace is reachable under `contract.namespace.<id>`; non-colliding namespace names are promoted to the root, with a type-level exclusion so a schema named like a contract field never shadows it. - Sync the SQLite test fixture pair on `capabilities.sql`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/1-framework/1-core/framework-components/src/ir/contract-view.ts`:
- Around line 132-145: `buildNamespaceAccessor` is using a plain object for raw
namespace ids, which can drop special keys like `__proto__`, and
`composeContractView` is checking namespace membership with `in`, which can be
confused by inherited properties like `constructor`. Update
`buildNamespaceAccessor` to use a null-prototype dictionary for the `out`
accumulator, and change the namespace lookup in `composeContractView` to
`Object.hasOwn(...)` so only real storage namespaces are considered before
promotion. Keep the existing `promoteBuiltinKinds` and `blindCast` flow intact.
In `@packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts`:
- Around line 15-18: The MongoContractView.fromJson() helper is hardcoded to new
MongoContractSerializer(), which prevents pack-contributed kinds and custom
validator fragments from being hydrated. Update MongoContractView.fromJson() to
accept and pass through a serializer/config parameter (or otherwise make the
method explicitly base-only) so callers can supply the correct serializer for
their contract shape, and keep the generic TContract aligned with the actual
hydration path.
In `@packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts`:
- Around line 45-49: The `MongoContractView.fromJson()` test is asserting the
wrong storage hash location, so update the assertion to check the contract root
field instead of `view.storage.storageHash`. Use the `fromJson()` call in
`mongo-contract-view.test.ts` and compare `view.storageHash` against
`contract.storageHash` to verify the raw round-trip shape is preserved.
In `@packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts`:
- Around line 38-43: The fromJson() method in PostgresContractView is hardcoding
a new PostgresContractSerializer(), which drops the serializer extension point
needed for pack-backed contracts. Update PostgresContractView.fromJson() so
callers can provide a configured serializer or equivalent options, and use that
injected serializer for deserializeContract() instead of always constructing the
default one. Keep buildSqlSchemaQualifiedView() as the final wrapping step after
hydration succeeds.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 292ababd-0d94-4c8b-91ec-3355a35b01bd
📒 Files selected for processing (28)
examples/retail-store/migrations/app/20260513T0505_initial/migration.tsexamples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.tsexamples/retail-store/test/migration.test.tspackages/1-framework/1-core/framework-components/src/exports/ir.tspackages/1-framework/1-core/framework-components/src/ir/contract-view.tspackages/1-framework/1-core/framework-components/test/contract-view.test.tspackages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.tspackages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.tspackages/2-mongo-family/9-family/package.jsonpackages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.tspackages/2-mongo-family/9-family/src/exports/ir.tspackages/2-mongo-family/9-family/test/fixtures/orm-contract.d.tspackages/2-mongo-family/9-family/test/fixtures/orm-contract.jsonpackages/2-mongo-family/9-family/test/mongo-contract-view.test-d.tspackages/2-mongo-family/9-family/test/mongo-contract-view.test.tspackages/2-mongo-family/9-family/tsconfig.test.jsonpackages/2-sql/1-core/contract/src/contract-view.tspackages/2-sql/1-core/contract/src/exports/contract-view.tspackages/3-targets/3-targets/postgres/src/core/postgres-contract-view.tspackages/3-targets/3-targets/postgres/src/exports/runtime.tspackages/3-targets/3-targets/postgres/test/fixtures/collision-contract.tspackages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.tspackages/3-targets/3-targets/postgres/test/postgres-contract-view.test.tspackages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.tspackages/3-targets/3-targets/sqlite/src/exports/runtime.tspackages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.jsonpackages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.tspackages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts
💤 Files with no reviewable changes (2)
- packages/3-targets/3-targets/postgres/src/exports/runtime.ts
- packages/3-targets/3-targets/sqlite/src/exports/runtime.ts
✅ Files skipped from review due to trivial changes (4)
- packages/2-mongo-family/9-family/tsconfig.test.json
- packages/2-sql/1-core/contract/src/exports/contract-view.ts
- packages/2-mongo-family/9-family/test/fixtures/orm-contract.d.ts
- packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json
🚧 Files skipped from review as they are similar to previous changes (2)
- examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts
- examples/retail-store/migrations/app/20260513T0505_initial/migration.ts
| const out: Record<string, unknown> = {}; | ||
| for (const [nsId, ns] of Object.entries(storage.namespaces)) { | ||
| out[nsId] = promoteBuiltinKinds( | ||
| blindCast< | ||
| Readonly<Record<string, unknown>>, | ||
| 'Namespace.entries is the open ADR 224 dictionary Record<string, Record<string, unknown>>' | ||
| >(ns.entries), | ||
| builtinKinds, | ||
| ); | ||
| } | ||
| return blindCast< | ||
| TAccessor, | ||
| 'each namespace projected to its SingleNamespaceView; keys mirror the storage namespace ids' | ||
| >(out); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
sed -n '128,187p' packages/1-framework/1-core/framework-components/src/ir/contract-view.ts
node <<'NODE'
const out = {};
out['__proto__'] = { polluted: true };
console.log('own keys:', Object.keys(out));
console.log('proto polluted:', Object.getPrototypeOf(out).polluted === true);
const root = {};
console.log('constructor via in:', 'constructor' in root);
console.log('constructor own:', Object.hasOwn(root, 'constructor'));
NODERepository: prisma/prisma-next
Length of output: 2599
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' packages/1-framework/1-core/framework-components/src/ir/contract-view.ts
printf '\n--- search ---\n'
rg -n "Object\.hasOwn| in root|Object\.create\(null\)|namespaceAccessor|buildNamespaceAccessor|composeContractView" packages/1-framework/1-core/framework-components/src/ir/contract-view.tsRepository: prisma/prisma-next
Length of output: 7906
Use null-prototype dictionaries for raw namespace ids.
buildNamespaceAccessordrops keys like__proto__on a plain{}.composeContractViewshould useObject.hasOwn(...)instead ofin, otherwise inherited names likeconstructorblock promotion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/1-framework/1-core/framework-components/src/ir/contract-view.ts`
around lines 132 - 145, `buildNamespaceAccessor` is using a plain object for raw
namespace ids, which can drop special keys like `__proto__`, and
`composeContractView` is checking namespace membership with `in`, which can be
confused by inherited properties like `constructor`. Update
`buildNamespaceAccessor` to use a null-prototype dictionary for the `out`
accumulator, and change the namespace lookup in `composeContractView` to
`Object.hasOwn(...)` so only real storage namespaces are considered before
promotion. Keep the existing `promoteBuiltinKinds` and `blindCast` flow intact.
| * const view = MongoContractView.fromJson<Contract>(contractJson); | ||
| * view.collection.carts.validator // typed MongoCollection | ||
| * view.entries.policy.X // pack-contributed kind (singular key) | ||
| * view.storage // the full contract is still present |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== MongoContractSerializer definition =="
fd 'mongo-contract-serializer.ts' packages/2-mongo-family/9-family/src --exec nl -ba
echo
echo "== Serializer base constructor / deserializeContract =="
nl -ba packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts | sed -n '1,140p'
echo
echo "== MongoContractView.fromJson call sites =="
rg -n --type=ts 'MongoContractView\.fromJson<|new MongoContractSerializer\(' packages examplesRepository: prisma/prisma-next
Length of output: 231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
git ls-files 'packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts' \
'packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts' \
'packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer.ts'
echo
echo "== mongo-contract-view.ts =="
sed -n '1,220p' packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts | cat -n
echo
echo "== mongo-contract-serializer-base.ts =="
sed -n '1,240p' packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer-base.ts | cat -n
echo
echo "== mongo-contract-serializer.ts =="
sed -n '1,220p' packages/2-mongo-family/9-family/src/core/ir/mongo-contract-serializer.ts | cat -n
echo
echo "== fromJson and serializer construction usages =="
rg -n --type=ts 'fromJson<|new MongoContractSerializer\(' packages/2-mongo-family/9-family/srcRepository: prisma/prisma-next
Length of output: 12658
fromJson() should accept serializer config
It always instantiates new MongoContractSerializer(), so pack-contributed kinds or custom validator fragments cannot be hydrated here even though the type parameter suggests any TContract. Either make this base-only or thread the serializer/config through.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts` around
lines 15 - 18, The MongoContractView.fromJson() helper is hardcoded to new
MongoContractSerializer(), which prevents pack-contributed kinds and custom
validator fragments from being hydrated. Update MongoContractView.fromJson() to
accept and pass through a serializer/config parameter (or otherwise make the
method explicitly base-only) so callers can supply the correct serializer for
their contract shape, and keep the generic TContract aligned with the actual
hydration path.
| it('fromJson() deserializes and wraps in one call', () => { | ||
| const view = MongoContractView.fromJson<Contract>(contractJson); | ||
| expect(view.collection.tasks).toBeDefined(); | ||
| expect(view.storage.storageHash).toBe(contract.storage.storageHash); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
This fromJson() assertion is checking the wrong storageHash path.
The emitted fixture stores storageHash at the contract root (packages/2-mongo-family/9-family/test/fixtures/orm-contract.json, Line 3), and the PR objective says Contract stays the raw round-trip shape. As written, Line 48 does not verify that fromJson() preserved the actual hash field. Assert view.storageHash against contract.storageHash instead.
Suggested fix
it('fromJson() deserializes and wraps in one call', () => {
const view = MongoContractView.fromJson<Contract>(contractJson);
expect(view.collection.tasks).toBeDefined();
- expect(view.storage.storageHash).toBe(contract.storage.storageHash);
+ expect(view.storageHash).toBe(contract.storageHash);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('fromJson() deserializes and wraps in one call', () => { | |
| const view = MongoContractView.fromJson<Contract>(contractJson); | |
| expect(view.collection.tasks).toBeDefined(); | |
| expect(view.storage.storageHash).toBe(contract.storage.storageHash); | |
| }); | |
| it('fromJson() deserializes and wraps in one call', () => { | |
| const view = MongoContractView.fromJson<Contract>(contractJson); | |
| expect(view.collection.tasks).toBeDefined(); | |
| expect(view.storageHash).toBe(contract.storageHash); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts` around
lines 45 - 49, The `MongoContractView.fromJson()` test is asserting the wrong
storage hash location, so update the assertion to check the contract root field
instead of `view.storage.storageHash`. Use the `fromJson()` call in
`mongo-contract-view.test.ts` and compare `view.storageHash` against
`contract.storageHash` to verify the raw round-trip shape is preserved.
| /** Deserialize a Postgres contract JSON envelope and wrap it in a view. */ | ||
| fromJson<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>( | ||
| json: unknown, | ||
| ): PostgresContractView<TContract> { | ||
| const contract = new PostgresContractSerializer().deserializeContract<TContract>(json); | ||
| return buildSqlSchemaQualifiedView(contract); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
fromJson() drops the serializer extension point.
This always builds a default PostgresContractSerializer(), so any emitted contract JSON that needs extraPackEntityKinds will fail during hydration before it ever reaches the view layer. Please let callers pass a configured serializer (or equivalent options) here.
Proposed fix
export const PostgresContractView = {
/** Wrap an already-deserialized Postgres contract in a schema-qualified view. */
from<TContract extends Contract<SqlStorage>>(
contract: TContract,
): PostgresContractView<TContract> {
return buildSqlSchemaQualifiedView(contract);
},
/** Deserialize a Postgres contract JSON envelope and wrap it in a view. */
fromJson<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>(
json: unknown,
+ serializer: PostgresContractSerializer = new PostgresContractSerializer(),
): PostgresContractView<TContract> {
- const contract = new PostgresContractSerializer().deserializeContract<TContract>(json);
+ const contract = serializer.deserializeContract<TContract>(json);
return buildSqlSchemaQualifiedView(contract);
},
};Based on learnings, unknown kinds are supposed to fail closed in serializer hydration, and the serializer constructor’s extra-kind hook is the intended extension point for pack-backed contracts.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Deserialize a Postgres contract JSON envelope and wrap it in a view. */ | |
| fromJson<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>( | |
| json: unknown, | |
| ): PostgresContractView<TContract> { | |
| const contract = new PostgresContractSerializer().deserializeContract<TContract>(json); | |
| return buildSqlSchemaQualifiedView(contract); | |
| /** Deserialize a Postgres contract JSON envelope and wrap it in a view. */ | |
| fromJson<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>( | |
| json: unknown, | |
| serializer: PostgresContractSerializer = new PostgresContractSerializer(), | |
| ): PostgresContractView<TContract> { | |
| const contract = serializer.deserializeContract<TContract>(json); | |
| return buildSqlSchemaQualifiedView(contract); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts`
around lines 38 - 43, The fromJson() method in PostgresContractView is
hardcoding a new PostgresContractSerializer(), which drops the serializer
extension point needed for pack-backed contracts. Update
PostgresContractView.fromJson() so callers can provide a configured serializer
or equivalent options, and use that injected serializer for
deserializeContract() instead of always constructing the default one. Keep
buildSqlSchemaQualifiedView() as the final wrapping step after hydration
succeeds.
Source: Learnings
Address review on #879. The view was a class with a private constructor and a static `from()` returning a separate mapped type — a namespace cosplaying as a class, and a narrow projection that was not substitutable for Contract. - Each `<Target>ContractView` is now a plain `{ from, fromJson }` factory; the classes are deleted. `fromJson` deserializes via the serializer and wraps in one step, so a migration declares the view once. - View type is now `Contract & accessors` (a superset) — one value serves as both the contract and the accessor surface. - Collision-safe namespace access: contract envelope fields always win at the root; every namespace is reachable under `contract.namespace.<id>`; non-colliding namespace names are promoted to the root, with a type-level exclusion so a schema named like a contract field never shadows it. - Sync the SQLite test fixture pair on `capabilities.sql`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
5c04541 to
d55131d
Compare
Address review on #879. The view was a class with a private constructor and a static `from()` returning a separate mapped type — a namespace cosplaying as a class, and a narrow projection that was not substitutable for Contract. - Each `<Target>ContractView` is now a plain `{ from, fromJson }` factory; the classes are deleted. `fromJson` deserializes via the serializer and wraps in one step, so a migration declares the view once. - View type is now `Contract & accessors` (a superset) — one value serves as both the contract and the accessor surface. - Collision-safe namespace access: contract envelope fields always win at the root; every namespace is reachable under `contract.namespace.<id>`; non-colliding namespace names are promoted to the root, with a type-level exclusion so a schema named like a contract field never shadows it. - Sync the SQLite test fixture pair on `capabilities.sql`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
d55131d to
0f9e4ef
Compare
…rop codemod Address wmadden's review on #879: - inline the minimal `{ storage: { storageHash } }` field shape (drop the vague `ContractHashInput` name), matching the existing repo idiom - drop stray `async` from `describe()` test blocks - typecheck family-mongo's tests via its main tsconfig like sibling packages (fix the pre-existing fixture type errors that surfaced); delete the special-case `tsconfig.test.json` - rewrite the mongo `render-typescript` doc comment to current behavior only - extract the lazy view-getter machinery into a shared `MigrationContractViews` helper (one copy, not three) with a structured `MIGRATION.CONTRACT_VIEW_MISSING` error instead of a bare `Error` - remove the one-shot `codemod-migration-shape.mjs` Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
91a1a4d to
1ab9dc1
Compare
1ab9dc1 to
781350d
Compare
Migration authors reached contract entities through internal coordinates (`storage.namespaces.__unbound__.entries.collection.<name>`), leaking the `__unbound__` sentinel, the `entries` wrapper, and the kind key. Add a per-target `ContractView` that presents entities by name with the default namespace unwrapped, while keeping `Contract` itself a faithful low-level mirror of the serialized form (no emitter/serializer/Contract-type changes). - Shared generic projection core in `framework-components` (`SingleNamespaceView` + `buildSingleNamespaceView` + `promoteBuiltinKinds`): promotes a family's statically-named built-in kind slots to typed top-level accessors, leaving pack-contributed kinds under `.entries` keyed by the registered singular kind string. - `MongoContractView.from(c)` — single namespace unwrapped: `cv.collection.<name>`. - `SqliteContractView.from(c)` — single namespace unwrapped: `cv.table.<name>`, `cv.valueSet.<name>`. - `PostgresContractView.from(c)` — schema-qualified, mirroring the `sql.<ns>` facade: `cv.<ns>.table.<name>`, `cv.<ns>.entries.policy.<name>`. Each view's factory is generic over the contract type, so access is fully typed against the emitted contract. The retail-store example migrations are repointed off the leaked path. Types are proven via emit-then-consume tests against real emitted contract `.d.ts` fixtures (incl. a multi-schema Postgres contract with a colliding bare table name). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
The retail-store example repoint triggers check:upgrade-coverage. The ContractView accessor is additive and the old raw path still works, so no consumer migration is required; record an incidental-diff note with changes: [] in the 0.14-to-0.15 transition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
Address review on #879. The view was a class with a private constructor and a static `from()` returning a separate mapped type — a namespace cosplaying as a class, and a narrow projection that was not substitutable for Contract. - Each `<Target>ContractView` is now a plain `{ from, fromJson }` factory; the classes are deleted. `fromJson` deserializes via the serializer and wraps in one step, so a migration declares the view once. - View type is now `Contract & accessors` (a superset) — one value serves as both the contract and the accessor surface. - Collision-safe namespace access: contract envelope fields always win at the root; every namespace is reachable under `contract.namespace.<id>`; non-colliding namespace names are promoted to the root, with a type-level exclusion so a schema named like a contract field never shadows it. - Sync the SQLite test fixture pair on `capabilities.sql`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…ction (#834) Retire the bespoke collision design and reuse the established namespace projection from #834: lift the duplicated `unboundNamespace` helper into framework-components and repoint the sqlite/mongo runtimes onto it; expose schemas under a single `namespace` member (`NamespacedEntities`, mirroring `NamespacedEnums`) so a schema named like a contract field can never shadow it. Single-namespace targets keep flat root kind accessors; Postgres is reached via `view.namespace.<schema>`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…+ exposes views
The `Migration` base now accepts the start/end contract JSON as typed inputs
(`startContractJson`/`endContractJson`) instead of hand-written from/to hashes:
`describe()` derives `{from, to}` from each contract's `storage.storageHash`
(byte-identical to the previous literals). The Mongo family base exposes lazy,
typed `this.startContract`/`this.endContract` ContractViews built from those
JSON inputs — so a migration gets typed, convenient contract access for free.
`describe()` becomes concrete-with-derive (throws if no JSON and no override),
so extension migrations that override it and carry no contract keep working.
retail-store converted to the new shape; migration.json/ops.json unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ract views Mirror D-A for the SQL family: thread `<Start, End>` generics through `SqlMigration` to the framework base, and add lazy memoized `startContract`/ `endContract` getters on `SqliteMigration` and `PostgresMigration`, each returning its own ContractView shape (`SqliteContractView` flat; `PostgresContractView` schema-qualified via `.namespace.<schema>`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…op describe() The three `render-typescript` generators now emit migrations that import their start/end contract JSON + types and extend `Migration<Start, End>` (baseline `Migration<never, End>`), instead of a hand-written `describe()` with inlined hashes. The base derives `describe()` from the JSON; the typed `startContract`/ `endContract` views are available on every generated migration. Inlined operations are unchanged. The JSON default-import reuses the same symbol the dataTransform op already imports, so the two merge into one import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…e new migration shape
Close out the migration-shape redesign:
- correct the stale Mongo `render-typescript` doc comment (describe() is now
derived from contract JSON, not rendered)
- `regen-example-migrations` tolerates the new shape (no from/to hash literals
to rewrite — they derive from the imported JSON)
- `no-bare-cast` Biome plugin anchors on `TsAsExpression` so `import type {
Contract as End }` aliases the generator emits are no longer flagged as casts
(repo cast count 1135 -> 1062; branch delta 0)
- integration journey-helper matches the new `Migration<Start,End>` scaffold
- record the extension-author upgrade-coverage entry for the unboundNamespace lift
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…N shape Every example migration (70) now uses the generator's contract-JSON shape — contract-JSON imports, `Migration<Start, End>` (baseline `<never, End>`), and a base-derived `describe()` — replacing the hand-written `describe()` hashes. The `operations` bodies are preserved verbatim, so `ops.json`/`migration.json` are byte-identical; the regeneration changes only the authored scaffold. The one data migration (retail-store backfill) keeps its hand-authored `dataTransform` reading `this.endContract` — the typed view's real consumer. Fixture migrations that lacked a `start-contract` gain one derived from their DAG predecessor's end-contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…import symbol Post-rebase reconciliation onto main's #880: - convert its new `20260628T0931` retail-store migration (pure-schema) to the contract-JSON shape (ops.json/migration.json byte-identical) - rename the JSON-import symbol `endContractJson` -> `endContract` on the two original retail-store migrations to match the generator's exact emission Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
…rop codemod Address wmadden's review on #879: - inline the minimal `{ storage: { storageHash } }` field shape (drop the vague `ContractHashInput` name), matching the existing repo idiom - drop stray `async` from `describe()` test blocks - typecheck family-mongo's tests via its main tsconfig like sibling packages (fix the pre-existing fixture type errors that surfaced); delete the special-case `tsconfig.test.json` - rewrite the mongo `render-typescript` doc comment to current behavior only - extract the lazy view-getter machinery into a shared `MigrationContractViews` helper (one copy, not three) with a structured `MIGRATION.CONTRACT_VIEW_MISSING` error instead of a bare `Error` - remove the one-shot `codemod-migration-shape.mjs` Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
ADR 232 records that a migration is authored against its start/end contract snapshots (identity derived from storage.storageHash; schema generated, data transforms hand-authored). ADR 233 records ContractView as a standalone typed, by-name accessor over a contract (superset via from/fromJson; schemas under a `namespace` member; projection shared with the runtime enums surface). The two cross-reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
781350d to
eb2c369
Compare
At a glance
A migration, before and after this PR:
For a migration that needs the contract (a data backfill), the view is right there, fully typed:
The decision
Extend the
Migrationbase to take the migration's start/end contract JSON as typed inputs instead of hand-written from/to hashes. The base:describe()—to = endContractJson.storage.storageHash,from = startContractJson?.storage.storageHash ?? null(byte-identical to the hashes that were previously copied in by hand), andthis.startContract/this.endContract, each aContractView: a superset of the contract that lets you reach entities by name (this.endContract.collection.carts) without spelling the__unbound__sentinel, theentrieswrapper, or the kind key.migration planemits this shape; every example migration is regenerated to it.Contractitself stays a raw, low-level mirror of the serialized form — the ergonomics live in the view.How it got here
The ticket started as "add an accessor so migration authors stop writing
storage.namespaces.__unbound__.entries.collection." Building the accessor surfaced the real question: generated migrations inline their values and never read the contract at all — so the only place that leak appeared was hand-authored migrations. An accessor alone would have been a solution looking for a problem.The fix that earns its place: make the contract a first-class input to every migration. The
Migrationbase reads the migration's own committed contract snapshot — which also lets it derive the from/to hashes (no more hand-copiedsha256:…) — and hands the author a typed view over it for the one thing that is hand-authored: data transforms. Schema content stays 100% generator output.The view
Per target,
ContractViewunwraps the target's default namespace and mirrors the establisheddb.enumsprojection (sharedunboundNamespacehelper,NamespacedEntitiesalongsideNamespacedEnums):this.endContract.collection.cartsthis.endContract.table.usersthis.endContract.namespace.public.table.usersThe view is a superset of the contract (so
this.endContractis usable as a contract and an accessor), built via a{ from, fromJson }factory — no class-that-doesn't-instantiate.What changed
Migrationbase (migration-tools) — generics<Start, End>, optionalstartContractJson/endContractJson, concrete deriveddescribe()(extension migrations that overridedescribe()and carry no contract keep working).MongoMigration,SqliteMigration,PostgresMigrationexpose lazy typedstartContract/endContractviews.ContractView—framework-componentsshared projection + per-target views (@prisma-next/family-mongo,@prisma-next/target-{sqlite,postgres}).render-typescriptgenerators — emit the new shape, drop thedescribe()emission.ops.json/migration.jsonare byte-identical.no-bare-castBiome plugin no longer misreadsimport type { X as Y }aliases as casts.Testing
Types are proven emit-then-consume against real emitted contract
.d.tsfixtures (including a multi-schema Postgres contract where reaching the wrong schema's column is a compile error). The decisive check:test:integration(1125 tests) executes the regenerated migration scaffolds end-to-end throughmigration apply, andops.json/migration.jsonare byte-identical across the whole regeneration — the scaffold changed, no migration's behavior did. Full CI-parity gates pass locally (build, force-typecheck, the whole Lint job incl. a delta-0 cast ratchet and upgrade-coverage,fixtures:check, and all three test suites).Alternatives considered
Contractitself. Rejected: the author-facing contract is data-only (the emitted.d.tshas no methods), so a getter there is invisible — and it would couple the emitter to a convenience concern.storage,domain) would silently shadow it. Schemas live under.namespaceinstead — matchingdb.enums.Refs: TML-2892
🤖 Generated with Claude Code