From 67ecd5b1ad6785f9556e2c71b86371aac168f35c Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 26 Jun 2026 18:37:30 +0200 Subject: [PATCH 01/12] feat(tml-2892): add ContractView migration-author accessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration authors reached contract entities through internal coordinates (`storage.namespaces.__unbound__.entries.collection.`), 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.`. - `SqliteContractView.from(c)` — single namespace unwrapped: `cv.table.`, `cv.valueSet.`. - `PostgresContractView.from(c)` — schema-qualified, mirroring the `sql.` facade: `cv..table.`, `cv..entries.policy.`. 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../app/20260513T0505_initial/migration.ts | 4 +- .../migration.ts | 5 +- examples/retail-store/test/migration.test.ts | 4 +- .../framework-components/src/exports/ir.ts | 2 + .../src/ir/contract-view.ts | 92 +++ .../test/contract-view.test.ts | 53 ++ .../mongo-contract/src/contract-view.ts | 47 ++ .../mongo-contract/src/exports/index.ts | 2 + .../test/contract-view.test-d.ts | 36 + .../mongo-contract/test/contract-view.test.ts | 65 ++ .../2-mongo-family/9-family/src/exports/ir.ts | 2 + packages/2-sql/1-core/contract/package.json | 1 + .../1-core/contract/src/contract-view.ts | 86 +++ .../contract/src/exports/contract-view.ts | 10 + .../2-sql/1-core/contract/tsdown.config.ts | 1 + .../src/core/postgres-contract-view.ts | 38 ++ .../3-targets/postgres/src/exports/runtime.ts | 2 + .../test/fixtures/namespaced-contract.d.ts | 335 +++++++++ .../test/fixtures/namespaced-contract.json | 257 +++++++ .../test/postgres-contract-view.test-d.ts | 60 ++ .../test/postgres-contract-view.test.ts | 92 +++ .../sqlite/src/core/sqlite-contract-view.ts | 35 + .../3-targets/sqlite/src/exports/runtime.ts | 2 + .../sqlite/test/fixtures/sqlite-contract.d.ts | 638 ++++++++++++++++++ .../sqlite/test/fixtures/sqlite-contract.json | 574 ++++++++++++++++ .../test/sqlite-contract-view.test-d.ts | 48 ++ .../sqlite/test/sqlite-contract-view.test.ts | 62 ++ 27 files changed, 2546 insertions(+), 7 deletions(-) create mode 100644 packages/1-framework/1-core/framework-components/src/ir/contract-view.ts create mode 100644 packages/1-framework/1-core/framework-components/test/contract-view.test.ts create mode 100644 packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts create mode 100644 packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts create mode 100644 packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts create mode 100644 packages/2-sql/1-core/contract/src/contract-view.ts create mode 100644 packages/2-sql/1-core/contract/src/exports/contract-view.ts create mode 100644 packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts create mode 100644 packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.d.ts create mode 100644 packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.json create mode 100644 packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts create mode 100644 packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts create mode 100644 packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.d.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json create mode 100644 packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts diff --git a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts index 12801a19a4..62edd66a8f 100755 --- a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractSerializer } from '@prisma-next/family-mongo/ir'; +import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createCollection, createIndex } from '@prisma-next/target-mongo/migration'; import type { Contract } from './end-contract'; @@ -8,7 +8,7 @@ import endContractJson from './end-contract.json' with { type: 'json' }; const endContract = new MongoContractSerializer().deserializeContract(endContractJson); -const COLLECTIONS = endContract.storage.namespaces.__unbound__.entries.collection; +const COLLECTIONS = MongoContractView.from(endContract).collection; const CARTS_VALIDATOR = COLLECTIONS.carts.validator; const EVENTS_VALIDATOR = COLLECTIONS.events.validator; const INVOICES_VALIDATOR = COLLECTIONS.invoices.validator; diff --git a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts index e7adf65d26..d36a621ef2 100755 --- a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractSerializer } from '@prisma-next/family-mongo/ir'; +import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { AggregateCommand, @@ -35,8 +35,7 @@ const STORAGE_HASH = endContract.storage.storageHash; // // The validator is sourced from `end-contract.json` so the op stays in sync // with the contract if the chain is ever re-emitted. -const PRODUCTS_VALIDATOR = - endContract.storage.namespaces.__unbound__.entries.collection.products.validator; +const PRODUCTS_VALIDATOR = MongoContractView.from(endContract).collection.products.validator; function existingProductsWithoutStatus(): MongoQueryPlan { return { diff --git a/examples/retail-store/test/migration.test.ts b/examples/retail-store/test/migration.test.ts index d15e39bc6a..0c6ac5fc17 100644 --- a/examples/retail-store/test/migration.test.ts +++ b/examples/retail-store/test/migration.test.ts @@ -1,4 +1,4 @@ -import { MongoContractSerializer } from '@prisma-next/family-mongo/ir'; +import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; import { timeouts } from '@prisma-next/test-utils'; import { type Db, MongoClient } from 'mongodb'; import { MongoMemoryReplSet } from 'mongodb-memory-server'; @@ -27,7 +27,7 @@ describe('migration', { timeout: timeouts.spinUpMongoMemoryServer }, () => { it('contract contains expected index definitions', () => { const contract = new MongoContractSerializer().deserializeContract(contractJson); - const collections = contract.storage.namespaces['__unbound__'].entries.collection; + const collections = MongoContractView.from(contract).collection; expect(collections.products.indexes).toMatchObject([ { diff --git a/packages/1-framework/1-core/framework-components/src/exports/ir.ts b/packages/1-framework/1-core/framework-components/src/exports/ir.ts index 7cbe407126..b45c117784 100644 --- a/packages/1-framework/1-core/framework-components/src/exports/ir.ts +++ b/packages/1-framework/1-core/framework-components/src/exports/ir.ts @@ -1,3 +1,5 @@ +export type { DefaultNamespaceEntries, SingleNamespaceView } from '../ir/contract-view'; +export { buildSingleNamespaceView, promoteBuiltinKinds } from '../ir/contract-view'; export { domainElementCoordinates } from '../ir/domain'; export type { AnyEntityKindDescriptor, EntityKindDescriptor } from '../ir/entity-kind'; export { hydrateNamespaceEntities } from '../ir/entity-kind'; diff --git a/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts new file mode 100644 index 0000000000..81b5c69e09 --- /dev/null +++ b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts @@ -0,0 +1,92 @@ +import { blindCast } from '@prisma-next/utils/casts'; +import { UNBOUND_NAMESPACE_ID } from './namespace'; +import type { Storage } from './storage'; + +/** + * Extracts the entries map of a contract's single default namespace + * (`UNBOUND_NAMESPACE_ID`). Both single-namespace families (Mongo, SQLite) + * store all entities under this one namespace. + */ +export type DefaultNamespaceEntries = + TStorage['namespaces'] extends Record + ? E + : never; + +/** + * Generic single-namespace projection shape. A family supplies: + * - `TEntries` — the family's `*NamespaceEntries` type for the default namespace. + * - `TBuiltinKinds` — the union of the family's statically-named built-in kind + * keys (Mongo `'collection'`; SQL `'table' | 'valueSet'`). + * + * Each built-in kind becomes a top-level accessor; the remaining pack-contributed + * kinds stay under `.entries` (keyed by their registered singular kind string). + * + * A built-in kind that the emitted contract does not carry resolves to an empty + * map (`Record`), matching the runtime which always materializes + * each built-in slot. The `& string` index-signature member of `TEntries` is + * excluded from `.entries` so only the literal pack-kind keys remain. + */ +export type SingleNamespaceView = { + readonly [K in TBuiltinKinds]-?: K extends keyof TEntries + ? NonNullable + : Record; +} & { + readonly entries: { + readonly [K in Exclude as string extends K + ? never + : K]: TEntries[K]; + }; +}; + +/** + * Projects one namespace's `entries` into the view shape: each built-in kind + * becomes a top-level slot (materialized empty if absent), and the remaining + * pack-contributed kinds sit under `.entries`. Shared by the single-namespace + * builder and the per-schema (multi-namespace) Postgres builder. + */ +export function promoteBuiltinKinds( + entries: Readonly>, + builtinKinds: readonly string[], +): TView { + const view: Record = {}; + const rest: Record = {}; + for (const [kind, kindMap] of Object.entries(entries)) { + if (builtinKinds.includes(kind)) { + view[kind] = kindMap; + } else { + rest[kind] = kindMap; + } + } + for (const kind of builtinKinds) { + if (!(kind in view)) { + view[kind] = {}; + } + } + view['entries'] = rest; + return blindCast( + view, + ); +} + +/** + * Builds the runtime projection object for a single-namespace contract: unwraps + * the default namespace and promotes the given built-in kind slots to top-level. + * The static type is supplied by the caller via the generic factory; this + * function is structural. + * + * Throws if the contract has no default (`UNBOUND_NAMESPACE_ID`) namespace. + */ +export function buildSingleNamespaceView( + storage: Storage, + builtinKinds: readonly string[], +): TView { + const defaultNs = storage.namespaces[UNBOUND_NAMESPACE_ID]; + if (defaultNs === undefined) { + throw new Error(`ContractView: contract has no default namespace (${UNBOUND_NAMESPACE_ID})`); + } + const entries = blindCast< + Record, + 'Namespace.entries is the open ADR 224 dictionary Record>' + >(defaultNs.entries); + return promoteBuiltinKinds(entries, builtinKinds); +} diff --git a/packages/1-framework/1-core/framework-components/test/contract-view.test.ts b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts new file mode 100644 index 0000000000..c9edafd08e --- /dev/null +++ b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { buildSingleNamespaceView } from '../src/ir/contract-view'; +import { UNBOUND_NAMESPACE_ID } from '../src/ir/namespace'; +import type { Storage } from '../src/ir/storage'; + +function storageWith(entries: Record): Storage { + return { + namespaces: { + [UNBOUND_NAMESPACE_ID]: { id: UNBOUND_NAMESPACE_ID, kind: 'test-namespace', entries }, + }, + } as unknown as Storage; +} + +describe('buildSingleNamespaceView', () => { + it('promotes built-in kinds to top-level and keeps pack kinds under .entries', () => { + const table = { users: { name: 'users' } }; + const policy = { readAll: { name: 'readAll' } }; + const view = buildSingleNamespaceView<{ + table: typeof table; + valueSet: Record; + entries: { policy: typeof policy }; + }>(storageWith({ table, policy }), ['table', 'valueSet']); + + expect(view.table).toBe(table); + expect(view.entries.policy).toBe(policy); + expect(Object.keys(view.entries)).toEqual(['policy']); + }); + + it('materializes a missing built-in kind as an empty map', () => { + const view = buildSingleNamespaceView<{ + table: Record; + valueSet: Record; + entries: Record; + }>(storageWith({ table: { t: {} } }), ['table', 'valueSet']); + + expect(view.valueSet).toEqual({}); + }); + + it('.entries excludes every built-in kind', () => { + const view = buildSingleNamespaceView<{ + table: Record; + valueSet: Record; + entries: Record; + }>(storageWith({ table: {}, valueSet: {}, policy: {} }), ['table', 'valueSet']); + + expect(Object.keys(view.entries)).toEqual(['policy']); + }); + + it('throws when the contract has no default namespace', () => { + const storage = { namespaces: {} } as unknown as Storage; + expect(() => buildSingleNamespaceView(storage, ['table'])).toThrow(/default namespace/); + }); +}); diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts new file mode 100644 index 0000000000..60eba70913 --- /dev/null +++ b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts @@ -0,0 +1,47 @@ +import { + buildSingleNamespaceView, + type DefaultNamespaceEntries, + type SingleNamespaceView, +} from '@prisma-next/framework-components/ir'; +import type { MongoContract } from './contract-types'; + +const MONGO_BUILTIN_KINDS = ['collection'] as const; +type MongoBuiltinKind = (typeof MONGO_BUILTIN_KINDS)[number]; + +type MongoEntries = DefaultNamespaceEntries; + +export type MongoContractViewShape = SingleNamespaceView< + MongoEntries, + MongoBuiltinKind +>; + +/** + * A read-only view over a deserialized Mongo contract that unwraps the + * default namespace and promotes the built-in `collection` kind to the + * top level. + * + * Usage: + * ```ts + * const cv = MongoContractView.from(endContract); + * cv.collection.carts // typed MongoCollection + * cv.entries.policy.X // pack-contributed kind (singular key) + * ``` + * + * The `Contract` type is unchanged — this view is a separate object layered + * on top of the raw deserialized contract. The default-namespace unwrap and + * built-in-kind promotion are the generic single-namespace projection from + * `@prisma-next/framework-components`; Mongo only supplies its built-in kind + * set (`collection`). + */ +export class MongoContractView { + private constructor() {} + + static from( + contract: TContract, + ): MongoContractViewShape { + return buildSingleNamespaceView>( + contract.storage, + MONGO_BUILTIN_KINDS, + ); + } +} diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts b/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts index 4f9d3c7235..e6c66cf936 100644 --- a/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts +++ b/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts @@ -31,6 +31,8 @@ export type { MongoWildcardProjection, RootModelName, } from '../contract-types'; +export type { MongoContractViewShape } from '../contract-view'; +export { MongoContractView } from '../contract-view'; export { defaultMongoDomainNamespaceId, defaultMongoStorageNamespaceId, diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts new file mode 100644 index 0000000000..a5b0c3c2aa --- /dev/null +++ b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts @@ -0,0 +1,36 @@ +import { expectTypeOf, test } from 'vitest'; +import type { MongoContractView } from '../src/contract-view'; +import type { MongoCollection } from '../src/ir/mongo-collection'; +import type { Contract } from './fixtures/orm-contract.d'; + +/** + * Emit-then-consume type tests: the `Contract` type is the real emitted + * contract from `test/fixtures/orm-contract.d.ts`. All assertions check the + * projected type against the actual emitted shape, not a hand-authored + * `typeof` expression. + */ + +test('MongoContractView.from returns a MongoContractView', () => { + type CV = ReturnType>; + expectTypeOf().toMatchTypeOf<{ collection: object; entries: object }>(); +}); + +test('cv.collection gives correctly typed built-in collection entities', () => { + type CV = ReturnType>; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); +}); + +test('a non-existent collection name is a compile error', () => { + type CV = ReturnType>; + const cv = null as unknown as CV; + // @ts-expect-error 'nonexistent' does not exist on the collection map + cv.collection.nonexistent; +}); + +test('cv.entries does not contain the collection key', () => { + type CV = ReturnType>; + type Entries = CV['entries']; + type HasCollection = 'collection' extends keyof Entries ? true : false; + expectTypeOf().toEqualTypeOf(); +}); diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts new file mode 100644 index 0000000000..0f5126c1c6 --- /dev/null +++ b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { MongoContractView } from '../src/contract-view'; +import type { Contract } from './fixtures/orm-contract.d'; +import contractJson from './fixtures/orm-contract.json' with { type: 'json' }; + +// The fixture JSON is typed as the emitted `Contract` so the view's projected +// types are exercised. These tests assert runtime property access on the view, +// not contract deserialization. +const contract = contractJson as unknown as Contract; + +describe('MongoContractView', () => { + it('from() returns a view object', () => { + const cv = MongoContractView.from(contract); + expect(cv).toBeDefined(); + }); + + it('cv.collection exposes collections from the default namespace', () => { + const cv = MongoContractView.from(contract); + expect(cv.collection).toBeDefined(); + expect(cv.collection.tasks).toBeDefined(); + expect(cv.collection.users).toBeDefined(); + }); + + it('cv.collection. returns the MongoCollection entity', () => { + const cv = MongoContractView.from(contract); + expect(cv.collection.tasks).toBe( + contract.storage.namespaces['__unbound__'].entries['collection']['tasks'], + ); + expect(cv.collection.users).toBe( + contract.storage.namespaces['__unbound__'].entries['collection']['users'], + ); + }); + + it('cv.entries does not contain the collection key', () => { + const cv = MongoContractView.from(contract); + expect(Object.keys(cv.entries)).not.toContain('collection'); + }); + + it('cv.entries exposes pack-contributed kinds', () => { + // The fixture only carries the built-in `collection` kind, so this test + // hand-builds a contract with an extra pack-contributed `policy` kind to + // prove non-built-in kinds land under `.entries`. + const fakeEntry = { name: 'test-pack-entity' }; + const contractWithPackKind = { + ...contract, + storage: { + ...contract.storage, + namespaces: { + __unbound__: { + ...contract.storage.namespaces['__unbound__'], + entries: { + ...contract.storage.namespaces['__unbound__'].entries, + policy: { readPolicy: fakeEntry }, + }, + }, + }, + }, + } as unknown as Contract; + + const cv = MongoContractView.from(contractWithPackKind); + expect((cv.entries as Record)['policy']).toEqual({ + readPolicy: fakeEntry, + }); + }); +}); diff --git a/packages/2-mongo-family/9-family/src/exports/ir.ts b/packages/2-mongo-family/9-family/src/exports/ir.ts index 1743271326..0a1cd44a71 100644 --- a/packages/2-mongo-family/9-family/src/exports/ir.ts +++ b/packages/2-mongo-family/9-family/src/exports/ir.ts @@ -1,3 +1,5 @@ +export type { MongoContractViewShape } from '@prisma-next/mongo-contract'; +export { MongoContractView } from '@prisma-next/mongo-contract'; export { MongoContractSerializer } from '../core/ir/mongo-contract-serializer'; export { MongoContractSerializerBase } from '../core/ir/mongo-contract-serializer-base'; export { MongoSchemaVerifierBase } from '../core/ir/mongo-schema-verifier-base'; diff --git a/packages/2-sql/1-core/contract/package.json b/packages/2-sql/1-core/contract/package.json index 5c85cf5cdf..bac9773996 100644 --- a/packages/2-sql/1-core/contract/package.json +++ b/packages/2-sql/1-core/contract/package.json @@ -43,6 +43,7 @@ ], "exports": { "./canonicalization-hooks": "./dist/canonicalization-hooks.mjs", + "./contract-view": "./dist/contract-view.mjs", "./entity-kinds": "./dist/entity-kinds.mjs", "./factories": "./dist/factories.mjs", "./index-type-validation": "./dist/index-type-validation.mjs", diff --git a/packages/2-sql/1-core/contract/src/contract-view.ts b/packages/2-sql/1-core/contract/src/contract-view.ts new file mode 100644 index 0000000000..b010e9cd7e --- /dev/null +++ b/packages/2-sql/1-core/contract/src/contract-view.ts @@ -0,0 +1,86 @@ +import type { Contract } from '@prisma-next/contract/types'; +import { + buildSingleNamespaceView, + type DefaultNamespaceEntries, + promoteBuiltinKinds, + type SingleNamespaceView, +} from '@prisma-next/framework-components/ir'; +import { blindCast } from '@prisma-next/utils/casts'; +import type { SqlStorage } from './ir/sql-storage'; + +/** + * The SQL family's statically-named built-in entity kinds. `table` and + * `valueSet` are promoted to top-level view accessors; pack-contributed kinds + * (e.g. `policy`) stay under `.entries`. + */ +export const SQL_BUILTIN_KINDS = ['table', 'valueSet'] as const; +export type SqlBuiltinKind = (typeof SQL_BUILTIN_KINDS)[number]; + +type SqlEntries> = DefaultNamespaceEntries< + TContract['storage'] +>; + +/** + * Single-namespace SQL view shape: `cv.table.` and `cv.valueSet.` + * top-level, pack kinds under `cv.entries.`. A target that never emits a + * given built-in kind (SQLite has `sql.enums: false`, so it emits no + * `valueSet`) resolves that slot to an empty map. + */ +export type SqlSingleNamespaceViewShape> = + SingleNamespaceView, SqlBuiltinKind>; + +/** + * Builds the single-namespace SQL view: unwraps the default namespace and + * promotes the SQL built-in kinds (`table`, `valueSet`). Targets with one + * default namespace (SQLite) call this directly; Postgres qualifies by schema + * and does not use it. + */ +export function buildSqlSingleNamespaceView>( + contract: TContract, +): SqlSingleNamespaceViewShape { + return buildSingleNamespaceView>( + contract.storage, + SQL_BUILTIN_KINDS, + ); +} + +/** + * Schema-qualified SQL view shape: each storage namespace key (`public`, + * `auth`, the default `__unbound__`, …) maps to its own single-namespace view, + * mirroring the facade's `sql..` keying exactly — including the + * literal `__unbound__` key for the default schema (the facade does not rename + * it). Within each schema, `cv..table.` / `cv..valueSet.` + * are top-level and pack kinds sit under `cv..entries.`. + */ +export type SqlSchemaQualifiedViewShape> = { + readonly [Ns in keyof TContract['storage']['namespaces']]: TContract['storage']['namespaces'][Ns] extends { + readonly entries: infer E; + } + ? SingleNamespaceView + : never; +}; + +/** + * Builds the schema-qualified SQL view: one single-namespace projection per + * storage namespace, keyed by the raw namespace id (no renaming of the default + * schema). Postgres uses this; SQLite (single default namespace) uses + * {@link buildSqlSingleNamespaceView}. + */ +export function buildSqlSchemaQualifiedView>( + contract: TContract, +): SqlSchemaQualifiedViewShape { + const out: Record = {}; + for (const [nsId, ns] of Object.entries(contract.storage.namespaces)) { + out[nsId] = promoteBuiltinKinds( + blindCast< + Readonly>, + 'Namespace.entries is the open ADR 224 dictionary Record>' + >(ns.entries), + SQL_BUILTIN_KINDS, + ); + } + return blindCast< + SqlSchemaQualifiedViewShape, + 'each namespace projected to its SingleNamespaceView; keys mirror the storage namespace ids' + >(out); +} diff --git a/packages/2-sql/1-core/contract/src/exports/contract-view.ts b/packages/2-sql/1-core/contract/src/exports/contract-view.ts new file mode 100644 index 0000000000..af47db90b0 --- /dev/null +++ b/packages/2-sql/1-core/contract/src/exports/contract-view.ts @@ -0,0 +1,10 @@ +export type { + SqlBuiltinKind, + SqlSchemaQualifiedViewShape, + SqlSingleNamespaceViewShape, +} from '../contract-view'; +export { + buildSqlSchemaQualifiedView, + buildSqlSingleNamespaceView, + SQL_BUILTIN_KINDS, +} from '../contract-view'; diff --git a/packages/2-sql/1-core/contract/tsdown.config.ts b/packages/2-sql/1-core/contract/tsdown.config.ts index 79865e3aec..c09d4c0ac7 100644 --- a/packages/2-sql/1-core/contract/tsdown.config.ts +++ b/packages/2-sql/1-core/contract/tsdown.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from '@prisma-next/tsdown'; export default defineConfig({ entry: [ + 'src/exports/contract-view.ts', 'src/exports/entity-kinds.ts', 'src/exports/referential-action-sql.ts', 'src/exports/resolve-storage-table.ts', diff --git a/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts new file mode 100644 index 0000000000..cde2a6d62d --- /dev/null +++ b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts @@ -0,0 +1,38 @@ +import type { Contract } from '@prisma-next/contract/types'; +import { + buildSqlSchemaQualifiedView, + type SqlSchemaQualifiedViewShape, +} from '@prisma-next/sql-contract/contract-view'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; + +export type PostgresContractViewShape> = + SqlSchemaQualifiedViewShape; + +/** + * A read-only, schema-qualified view over a deserialized Postgres contract. + * Postgres has named schemas (`public`, `auth`, …) plus the default + * `__unbound__` schema, so the view qualifies by schema first, then applies the + * SQL kind split per schema: + * + * ```ts + * const cv = PostgresContractView.from(endContract); + * cv.public.table.users // typed table leaf in the public schema + * cv.auth.table.users // the auth schema's own users table + * cv.public.entries.policy.X // pack-contributed kind (RLS / #771 path) + * cv.__unbound__.table.X // default schema, keyed by its raw id + * ``` + * + * The schema keys mirror the facade's `sql.` keying exactly — the default + * schema is reachable under its literal `__unbound__` id, with no renaming. + * Each schema is its own key; there is no flat cross-namespace merge. The + * `Contract` type is unchanged — this view is a separate object layered on top. + */ +export class PostgresContractView { + private constructor() {} + + static from>( + contract: TContract, + ): PostgresContractViewShape { + return buildSqlSchemaQualifiedView(contract); + } +} diff --git a/packages/3-targets/3-targets/postgres/src/exports/runtime.ts b/packages/3-targets/3-targets/postgres/src/exports/runtime.ts index e40c26e32d..21e44b3e4e 100644 --- a/packages/3-targets/3-targets/postgres/src/exports/runtime.ts +++ b/packages/3-targets/3-targets/postgres/src/exports/runtime.ts @@ -6,6 +6,8 @@ import type { import { postgresTargetDescriptorMetaRuntime } from '../core/descriptor-meta-runtime'; export { PostgresContractSerializer } from '../core/postgres-contract-serializer'; +export type { PostgresContractViewShape } from '../core/postgres-contract-view'; +export { PostgresContractView } from '../core/postgres-contract-view'; export interface PostgresRuntimeTargetInstance extends RuntimeTargetInstance<'sql', 'postgres'> {} diff --git a/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.d.ts b/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.d.ts new file mode 100644 index 0000000000..d7b36e206f --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.d.ts @@ -0,0 +1,335 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:718881dff463472bd68e0d58a0f4edcc04bdafe806078e16c2e06c9425b78b7b'>; +export type ExecutionHash = ExecutionHashBase; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly auth: { + readonly User: { + readonly id: CodecTypes['pg/int4@1']['output']; + readonly token: CodecTypes['pg/text@1']['output']; + }; + }; + readonly public: { + readonly Profile: { + readonly id: CodecTypes['pg/int4@1']['output']; + readonly userId: CodecTypes['pg/int4@1']['output']; + }; + readonly User: { + readonly id: CodecTypes['pg/int4@1']['output']; + readonly email: CodecTypes['pg/text@1']['output']; + }; + }; +}; +export type FieldInputTypes = { + readonly auth: { + readonly User: { + readonly id: CodecTypes['pg/int4@1']['input']; + readonly token: CodecTypes['pg/text@1']['input']; + }; + }; + readonly public: { + readonly Profile: { + readonly id: CodecTypes['pg/int4@1']['input']; + readonly userId: CodecTypes['pg/int4@1']['input']; + }; + readonly User: { + readonly id: CodecTypes['pg/int4@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; + }; +}; +export type StorageColumnTypes = { + readonly auth: { + readonly users: { + readonly id: CodecTypes['pg/int4@1']['output']; + readonly token: CodecTypes['pg/text@1']['output']; + }; + }; + readonly public: { + readonly profile: { + readonly id: CodecTypes['pg/int4@1']['output']; + readonly user_id: CodecTypes['pg/int4@1']['output']; + }; + readonly users: { + readonly email: CodecTypes['pg/text@1']['output']; + readonly id: CodecTypes['pg/int4@1']['output']; + }; + }; +}; +export type StorageColumnInputTypes = { + readonly auth: { + readonly users: { + readonly id: CodecTypes['pg/int4@1']['input']; + readonly token: CodecTypes['pg/text@1']['input']; + }; + }; + readonly public: { + readonly profile: { + readonly id: CodecTypes['pg/int4@1']['input']; + readonly user_id: CodecTypes['pg/int4@1']['input']; + }; + readonly users: { + readonly email: CodecTypes['pg/text@1']['input']; + readonly id: CodecTypes['pg/int4@1']['input']; + }; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes, + StorageColumnTypes, + StorageColumnInputTypes +>; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly auth: { + readonly id: 'auth'; + readonly kind: 'postgres-schema'; + readonly entries: { + readonly table: { + readonly users: { + columns: { + readonly id: { + readonly nativeType: 'int4'; + readonly codecId: 'pg/int4@1'; + readonly nullable: false; + }; + readonly token: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly public: { + readonly id: 'public'; + readonly kind: 'postgres-schema'; + readonly entries: { + readonly table: { + readonly profile: { + columns: { + readonly id: { + readonly nativeType: 'int4'; + readonly codecId: 'pg/int4@1'; + readonly nullable: false; + }; + readonly user_id: { + readonly nativeType: 'int4'; + readonly codecId: 'pg/int4@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly [ + { + readonly source: { + readonly namespaceId: 'public' & NamespaceId; + readonly tableName: 'profile'; + readonly columns: readonly ['user_id']; + }; + readonly target: { + readonly namespaceId: 'auth' & NamespaceId; + readonly tableName: 'users'; + readonly columns: readonly ['id']; + }; + readonly constraint: true; + readonly index: true; + }, + ]; + }; + readonly users: { + columns: { + readonly id: { + readonly nativeType: 'int4'; + readonly codecId: 'pg/int4@1'; + readonly nullable: false; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly 'public.users': { readonly namespace: 'public' & NamespaceId; readonly model: 'User' }; + readonly profile: { readonly namespace: 'public' & NamespaceId; readonly model: 'Profile' }; + readonly 'auth.users': { readonly namespace: 'auth' & NamespaceId; readonly model: 'User' }; + }; + readonly domain: { + readonly namespaces: { + readonly auth: { + readonly models: { + readonly User: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/int4@1' }; + }; + readonly token: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'users'; + readonly namespaceId: 'auth'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly token: { readonly column: 'token' }; + }; + }; + }; + }; + }; + readonly public: { + readonly models: { + readonly Profile: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/int4@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/int4@1' }; + }; + }; + readonly relations: { + readonly user: { + readonly to: { readonly namespace: 'auth' & NamespaceId; readonly model: 'User' }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'profile'; + readonly namespaceId: 'public'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly userId: { readonly column: 'user_id' }; + }; + }; + }; + readonly User: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/int4@1' }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'users'; + readonly namespaceId: 'public'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + readonly scalarList: true; + }; + }; + readonly extensionPacks: {}; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; diff --git a/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.json b/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.json new file mode 100644 index 0000000000..29e781e0a1 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/fixtures/namespaced-contract.json @@ -0,0 +1,257 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "auth.users": { + "model": "User", + "namespace": "auth" + }, + "profile": { + "model": "Profile", + "namespace": "public" + }, + "public.users": { + "model": "User", + "namespace": "public" + } + }, + "domain": { + "namespaces": { + "auth": { + "models": { + "User": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "pg/int4@1", + "kind": "scalar" + } + }, + "token": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "token": { + "column": "token" + } + }, + "namespaceId": "auth", + "table": "users" + } + } + } + }, + "public": { + "models": { + "Profile": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "pg/int4@1", + "kind": "scalar" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "pg/int4@1", + "kind": "scalar" + } + } + }, + "relations": { + "user": { + "cardinality": "N:1", + "on": { + "localFields": ["userId"], + "targetFields": ["id"] + }, + "to": { + "model": "User", + "namespace": "auth" + } + } + }, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "userId": { + "column": "user_id" + } + }, + "namespaceId": "public", + "table": "profile" + } + }, + "User": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "pg/int4@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "namespaceId": "public", + "table": "users" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "auth": { + "entries": { + "table": { + "users": { + "columns": { + "id": { + "codecId": "pg/int4@1", + "nativeType": "int4", + "nullable": false + }, + "token": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + } + } + }, + "id": "auth", + "kind": "postgres-schema" + }, + "public": { + "entries": { + "table": { + "profile": { + "columns": { + "id": { + "codecId": "pg/int4@1", + "nativeType": "int4", + "nullable": false + }, + "user_id": { + "codecId": "pg/int4@1", + "nativeType": "int4", + "nullable": false + } + }, + "foreignKeys": [ + { + "constraint": true, + "index": true, + "source": { + "columns": ["user_id"], + "namespaceId": "public", + "tableName": "profile" + }, + "target": { + "columns": ["id"], + "namespaceId": "auth", + "tableName": "users" + } + } + ], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "users": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "pg/int4@1", + "nativeType": "int4", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + } + } + }, + "id": "public", + "kind": "postgres-schema" + } + }, + "storageHash": "sha256:718881dff463472bd68e0d58a0f4edcc04bdafe806078e16c2e06c9425b78b7b" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true, + "scalarList": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts new file mode 100644 index 0000000000..7975e9e0f6 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts @@ -0,0 +1,60 @@ +import { expectTypeOf, test } from 'vitest'; +import type { PostgresContractView } from '../src/core/postgres-contract-view'; +import type { Contract } from './fixtures/namespaced-contract.d'; + +/** + * Emit-then-consume type tests against a REAL emitted multi-schema Postgres + * contract (`test/fixtures/namespaced-contract.d.ts`). The fixture declares the + * SAME bare table `users` in BOTH `public` (column `email`) and `auth` (column + * `token`) — the discriminator that proves per-schema qualification. + */ + +type CV = ReturnType>; + +test('each schema is its own key with its own table columns', () => { + expectTypeOf< + CV['public']['table']['users']['columns']['email']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); + expectTypeOf< + CV['auth']['table']['users']['columns']['token']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); +}); + +test('cross-schema column access is a compile error', () => { + const cv = null as unknown as CV; + // @ts-expect-error public.users has no `token` column (that is auth.users) + cv.public.table.users.columns.token; + // @ts-expect-error auth.users has no `email` column (that is public.users) + cv.auth.table.users.columns.email; +}); + +test('a non-existent schema key is a compile error', () => { + const cv = null as unknown as CV; + // @ts-expect-error 'marketing' is not an emitted schema + cv.marketing; +}); + +test('a non-existent table name in a schema is a compile error', () => { + const cv = null as unknown as CV; + // @ts-expect-error 'orders' is not a table in the public schema + cv.public.table.orders; +}); + +test('the cross-schema foreign key on public.profile is reachable', () => { + expectTypeOf< + CV['public']['table']['profile']['foreignKeys'][0]['target']['namespaceId'] + >().toEqualTypeOf<'auth' & import('@prisma-next/contract/types').NamespaceId>(); +}); + +test('valueSet slot is present per schema (none emitted, so empty maps)', () => { + expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf>(); +}); + +test('cv..entries excludes the built-in table and valueSet keys', () => { + type PublicEntries = CV['public']['entries']; + type HasTable = 'table' extends keyof PublicEntries ? true : false; + type HasValueSet = 'valueSet' extends keyof PublicEntries ? true : false; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); +}); diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts new file mode 100644 index 0000000000..28cb28b996 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; +import { PostgresContractSerializer } from '../src/core/postgres-contract-serializer'; +import { PostgresContractView } from '../src/core/postgres-contract-view'; +import type { Contract } from './fixtures/namespaced-contract.d'; +import contractJson from './fixtures/namespaced-contract.json' with { type: 'json' }; + +const contract = new PostgresContractSerializer().deserializeContract(contractJson); + +describe('PostgresContractView', () => { + it('from() returns a view object', () => { + expect(PostgresContractView.from(contract)).toBeDefined(); + }); + + it('keys each schema separately with its own tables', () => { + const cv = PostgresContractView.from(contract); + expect(cv.public.table.users).toBeDefined(); + expect(cv.auth.table.users).toBeDefined(); + expect(Object.keys(cv.public.table.users.columns).sort()).toEqual(['email', 'id']); + expect(Object.keys(cv.auth.table.users.columns).sort()).toEqual(['id', 'token']); + }); + + it('cv..table. returns the same entity object as the raw contract', () => { + const cv = PostgresContractView.from(contract); + expect(cv.public.table.users).toBe(contract.storage.namespaces['public']?.entries.table?.users); + expect(cv.auth.table.users).toBe(contract.storage.namespaces['auth']?.entries.table?.users); + }); + + it('cv..valueSet is present and empty (no value sets emitted)', () => { + const cv = PostgresContractView.from(contract); + expect(cv.public.valueSet).toEqual({}); + expect(cv.auth.valueSet).toEqual({}); + }); + + it('cv..entries excludes the built-in table and valueSet keys', () => { + const cv = PostgresContractView.from(contract); + expect(Object.keys(cv.public.entries)).not.toContain('table'); + expect(Object.keys(cv.public.entries)).not.toContain('valueSet'); + }); + + it('the default __unbound__ schema is keyed by its raw id (mirrors the facade)', () => { + // Mirror the facade's keying: the default schema is reachable under its raw + // `__unbound__` id, not a renamed key. Hand-built since the committed + // namespaced fixture uses only named schemas. + const withDefault = { + ...contract, + storage: { + ...contract.storage, + namespaces: { + ...contract.storage.namespaces, + __unbound__: { + id: '__unbound__', + kind: 'postgres-schema', + entries: { table: { widgets: { columns: {} } } }, + }, + }, + }, + } as unknown as Contract; + + const cv = PostgresContractView.from(withDefault) as Record< + string, + { table: Record } + >; + expect(cv['__unbound__']?.table['widgets']).toBeDefined(); + }); + + it('cv..entries exposes pack-contributed kinds', () => { + // RLS `policy` isn't in a committed fixture yet, so hand-build a contract + // with a pack-contributed `policy` kind under the public schema. + const fakePolicy = { name: 'read_all' }; + const withPolicy = { + ...contract, + storage: { + ...contract.storage, + namespaces: { + ...contract.storage.namespaces, + public: { + ...contract.storage.namespaces['public'], + entries: { + ...contract.storage.namespaces['public']?.entries, + policy: { readAll: fakePolicy }, + }, + }, + }, + }, + } as unknown as Contract; + + const cv = PostgresContractView.from(withPolicy); + expect((cv.public.entries as Record)['policy']).toEqual({ + readAll: fakePolicy, + }); + }); +}); diff --git a/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts b/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts new file mode 100644 index 0000000000..742f762e3f --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts @@ -0,0 +1,35 @@ +import type { Contract } from '@prisma-next/contract/types'; +import { + buildSqlSingleNamespaceView, + type SqlSingleNamespaceViewShape, +} from '@prisma-next/sql-contract/contract-view'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; + +export type SqliteContractViewShape> = + SqlSingleNamespaceViewShape; + +/** + * A read-only view over a deserialized SQLite contract that unwraps the single + * default namespace and promotes the SQL built-in kinds to the top level. + * + * Usage: + * ```ts + * const cv = SqliteContractView.from(endContract); + * cv.table.users // typed table leaf + * cv.entries.policy.X // pack-contributed kind (singular key) + * ``` + * + * SQLite has `sql.enums: false`, so it never emits `valueSet` entries; the + * `valueSet` slot is therefore an empty map. The `Contract` type is unchanged — + * this view is a separate object layered on top, reusing the generic + * single-namespace projection. + */ +export class SqliteContractView { + private constructor() {} + + static from>( + contract: TContract, + ): SqliteContractViewShape { + return buildSqlSingleNamespaceView(contract); + } +} diff --git a/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts b/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts index 0b27c7ed20..db7eec59c2 100644 --- a/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts +++ b/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts @@ -1,3 +1,5 @@ export type { SqliteRuntimeTargetInstance } from '../core/runtime-target'; export { default } from '../core/runtime-target'; export { SqliteContractSerializer } from '../core/sqlite-contract-serializer'; +export type { SqliteContractViewShape } from '../core/sqlite-contract-view'; +export { SqliteContractView } from '../core/sqlite-contract-view'; diff --git a/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.d.ts b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.d.ts new file mode 100644 index 0000000000..f511ecc7ad --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.d.ts @@ -0,0 +1,638 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { CodecTypes as SqliteTypes } from '@prisma-next/adapter-sqlite/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:713d00a72dcba37a15c259fc3932797150465fca8dfe672130391ce3a16cc973'>; +export type ExecutionHash = ExecutionHashBase; +export type ProfileHash = + ProfileHashBase<'sha256:3cc333ecad9f3f4c7229370a9d2c37e908cdce0f8d2e9fb132d50605b024eff2'>; + +export type CodecTypes = SqliteTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = Record; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly __unbound__: { + readonly Comment: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly body: CodecTypes['sqlite/text@1']['output']; + readonly postId: CodecTypes['sqlite/integer@1']['output']; + }; + readonly Item: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly name: CodecTypes['sqlite/text@1']['output']; + readonly label: CodecTypes['sqlite/text@1']['output']; + }; + readonly Post: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly title: CodecTypes['sqlite/text@1']['output']; + readonly userId: CodecTypes['sqlite/integer@1']['output']; + readonly views: CodecTypes['sqlite/integer@1']['output']; + }; + readonly Profile: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly userId: CodecTypes['sqlite/integer@1']['output']; + readonly bio: CodecTypes['sqlite/text@1']['output']; + }; + readonly TypedRow: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly active: CodecTypes['sqlite/integer@1']['output']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['output']; + readonly metadata: CodecTypes['sqlite/json@1']['output'] | null; + readonly label: CodecTypes['sqlite/text@1']['output']; + }; + readonly User: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly name: CodecTypes['sqlite/text@1']['output']; + readonly email: CodecTypes['sqlite/text@1']['output']; + readonly invitedById: CodecTypes['sqlite/integer@1']['output'] | null; + }; + }; +}; +export type FieldInputTypes = { + readonly __unbound__: { + readonly Comment: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly body: CodecTypes['sqlite/text@1']['input']; + readonly postId: CodecTypes['sqlite/integer@1']['input']; + }; + readonly Item: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly name: CodecTypes['sqlite/text@1']['input']; + readonly label: CodecTypes['sqlite/text@1']['input']; + }; + readonly Post: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly title: CodecTypes['sqlite/text@1']['input']; + readonly userId: CodecTypes['sqlite/integer@1']['input']; + readonly views: CodecTypes['sqlite/integer@1']['input']; + }; + readonly Profile: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly userId: CodecTypes['sqlite/integer@1']['input']; + readonly bio: CodecTypes['sqlite/text@1']['input']; + }; + readonly TypedRow: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly active: CodecTypes['sqlite/integer@1']['input']; + readonly createdAt: CodecTypes['sqlite/datetime@1']['input']; + readonly metadata: CodecTypes['sqlite/json@1']['input'] | null; + readonly label: CodecTypes['sqlite/text@1']['input']; + }; + readonly User: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly name: CodecTypes['sqlite/text@1']['input']; + readonly email: CodecTypes['sqlite/text@1']['input']; + readonly invitedById: CodecTypes['sqlite/integer@1']['input'] | null; + }; + }; +}; +export type StorageColumnTypes = { + readonly __unbound__: { + readonly comments: { + readonly body: CodecTypes['sqlite/text@1']['output']; + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly post_id: CodecTypes['sqlite/integer@1']['output']; + }; + readonly items: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly label: CodecTypes['sqlite/text@1']['output']; + readonly name: CodecTypes['sqlite/text@1']['output']; + }; + readonly posts: { + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly title: CodecTypes['sqlite/text@1']['output']; + readonly user_id: CodecTypes['sqlite/integer@1']['output']; + readonly views: CodecTypes['sqlite/integer@1']['output']; + }; + readonly profiles: { + readonly bio: CodecTypes['sqlite/text@1']['output']; + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly user_id: CodecTypes['sqlite/integer@1']['output']; + }; + readonly typed_rows: { + readonly active: CodecTypes['sqlite/integer@1']['output']; + readonly created_at: CodecTypes['sqlite/datetime@1']['output']; + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly label: CodecTypes['sqlite/text@1']['output']; + readonly metadata: CodecTypes['sqlite/json@1']['output'] | null; + }; + readonly users: { + readonly email: CodecTypes['sqlite/text@1']['output']; + readonly id: CodecTypes['sqlite/integer@1']['output']; + readonly invited_by_id: CodecTypes['sqlite/integer@1']['output'] | null; + readonly name: CodecTypes['sqlite/text@1']['output']; + }; + }; +}; +export type StorageColumnInputTypes = { + readonly __unbound__: { + readonly comments: { + readonly body: CodecTypes['sqlite/text@1']['input']; + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly post_id: CodecTypes['sqlite/integer@1']['input']; + }; + readonly items: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly label: CodecTypes['sqlite/text@1']['input']; + readonly name: CodecTypes['sqlite/text@1']['input']; + }; + readonly posts: { + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly title: CodecTypes['sqlite/text@1']['input']; + readonly user_id: CodecTypes['sqlite/integer@1']['input']; + readonly views: CodecTypes['sqlite/integer@1']['input']; + }; + readonly profiles: { + readonly bio: CodecTypes['sqlite/text@1']['input']; + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly user_id: CodecTypes['sqlite/integer@1']['input']; + }; + readonly typed_rows: { + readonly active: CodecTypes['sqlite/integer@1']['input']; + readonly created_at: CodecTypes['sqlite/datetime@1']['input']; + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly label: CodecTypes['sqlite/text@1']['input']; + readonly metadata: CodecTypes['sqlite/json@1']['input'] | null; + }; + readonly users: { + readonly email: CodecTypes['sqlite/text@1']['input']; + readonly id: CodecTypes['sqlite/integer@1']['input']; + readonly invited_by_id: CodecTypes['sqlite/integer@1']['input'] | null; + readonly name: CodecTypes['sqlite/text@1']['input']; + }; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes, + StorageColumnTypes, + StorageColumnInputTypes +>; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'sqlite-namespace'; + readonly entries: { + readonly table: { + readonly comments: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly body: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly post_id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly items: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly label: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + readonly default: { + readonly kind: 'literal'; + readonly value: DefaultLiteralValue<'sqlite/text@1', 'unnamed'>; + }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly posts: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly title: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly user_id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly views: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly profiles: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly user_id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly typed_rows: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly active: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly created_at: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/datetime@1'; + readonly nullable: false; + }; + readonly metadata: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/json@1'; + readonly nullable: true; + }; + readonly label: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly users: { + columns: { + readonly id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'sqlite/text@1'; + readonly nullable: false; + }; + readonly invited_by_id: { + readonly nativeType: 'integer'; + readonly codecId: 'sqlite/integer@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'sqlite'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly users: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; + readonly posts: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Post' }; + readonly comments: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Comment'; + }; + readonly profiles: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Profile'; + }; + readonly typed_rows: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'TypedRow'; + }; + readonly items: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Item' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly Comment: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly body: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly postId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'comments'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly body: { readonly column: 'body' }; + readonly postId: { readonly column: 'post_id' }; + }; + }; + }; + readonly Item: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'items'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly name: { readonly column: 'name' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; + readonly Post: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly title: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly views: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + }; + readonly relations: { + readonly comments: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Comment'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['postId']; + }; + }; + readonly author: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'User'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['id']; + }; + }; + }; + readonly storage: { + readonly table: 'posts'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly title: { readonly column: 'title' }; + readonly userId: { readonly column: 'user_id' }; + readonly views: { readonly column: 'views' }; + }; + }; + }; + readonly Profile: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly bio: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'profiles'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly userId: { readonly column: 'user_id' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + readonly TypedRow: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly active: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly createdAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/datetime@1' }; + }; + readonly metadata: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/json@1' }; + }; + readonly label: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'typed_rows'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly active: { readonly column: 'active' }; + readonly createdAt: { readonly column: 'created_at' }; + readonly metadata: { readonly column: 'metadata' }; + readonly label: { readonly column: 'label' }; + }; + }; + }; + readonly User: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/text@1' }; + }; + readonly invitedById: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'sqlite/integer@1' }; + }; + }; + readonly relations: { + readonly posts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Post'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['userId']; + }; + }; + readonly profile: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Profile'; + }; + readonly cardinality: '1:1'; + readonly on: { + readonly localFields: readonly ['id']; + readonly targetFields: readonly ['userId']; + }; + }; + }; + readonly storage: { + readonly table: 'users'; + readonly namespaceId: '__unbound__'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly name: { readonly column: 'name' }; + readonly email: { readonly column: 'email' }; + readonly invitedById: { readonly column: 'invited_by_id' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly sql: { + readonly enums: false; + readonly foreignKeys: true; + readonly jsonAgg: true; + readonly lateral: false; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; diff --git a/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json new file mode 100644 index 0000000000..eba0d49db1 --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json @@ -0,0 +1,574 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "sqlite", + "profileHash": "sha256:3cc333ecad9f3f4c7229370a9d2c37e908cdce0f8d2e9fb132d50605b024eff2", + "roots": { + "comments": { + "model": "Comment", + "namespace": "__unbound__" + }, + "items": { + "model": "Item", + "namespace": "__unbound__" + }, + "posts": { + "model": "Post", + "namespace": "__unbound__" + }, + "profiles": { + "model": "Profile", + "namespace": "__unbound__" + }, + "typed_rows": { + "model": "TypedRow", + "namespace": "__unbound__" + }, + "users": { + "model": "User", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "Comment": { + "fields": { + "body": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "postId": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "body": { + "column": "body" + }, + "id": { + "column": "id" + }, + "postId": { + "column": "post_id" + } + }, + "namespaceId": "__unbound__", + "table": "comments" + } + }, + "Item": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "label": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "label": { + "column": "label" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "__unbound__", + "table": "items" + } + }, + "Post": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "title": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "views": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + } + }, + "relations": { + "author": { + "cardinality": "N:1", + "on": { + "localFields": ["userId"], + "targetFields": ["id"] + }, + "to": { + "model": "User", + "namespace": "__unbound__" + } + }, + "comments": { + "cardinality": "1:N", + "on": { + "localFields": ["id"], + "targetFields": ["postId"] + }, + "to": { + "model": "Comment", + "namespace": "__unbound__" + } + } + }, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "title": { + "column": "title" + }, + "userId": { + "column": "user_id" + }, + "views": { + "column": "views" + } + }, + "namespaceId": "__unbound__", + "table": "posts" + } + }, + "Profile": { + "fields": { + "bio": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "id": { + "column": "id" + }, + "userId": { + "column": "user_id" + } + }, + "namespaceId": "__unbound__", + "table": "profiles" + } + }, + "TypedRow": { + "fields": { + "active": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "createdAt": { + "nullable": false, + "type": { + "codecId": "sqlite/datetime@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "label": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "metadata": { + "nullable": true, + "type": { + "codecId": "sqlite/json@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "active": { + "column": "active" + }, + "createdAt": { + "column": "created_at" + }, + "id": { + "column": "id" + }, + "label": { + "column": "label" + }, + "metadata": { + "column": "metadata" + } + }, + "namespaceId": "__unbound__", + "table": "typed_rows" + } + }, + "User": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "invitedById": { + "nullable": true, + "type": { + "codecId": "sqlite/integer@1", + "kind": "scalar" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "sqlite/text@1", + "kind": "scalar" + } + } + }, + "relations": { + "posts": { + "cardinality": "1:N", + "on": { + "localFields": ["id"], + "targetFields": ["userId"] + }, + "to": { + "model": "Post", + "namespace": "__unbound__" + } + }, + "profile": { + "cardinality": "1:1", + "on": { + "localFields": ["id"], + "targetFields": ["userId"] + }, + "to": { + "model": "Profile", + "namespace": "__unbound__" + } + } + }, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "invitedById": { + "column": "invited_by_id" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "__unbound__", + "table": "users" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "entries": { + "table": { + "comments": { + "columns": { + "body": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "post_id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "items": { + "columns": { + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "label": { + "codecId": "sqlite/text@1", + "default": { + "kind": "literal", + "value": "unnamed" + }, + "nativeType": "text", + "nullable": false + }, + "name": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "posts": { + "columns": { + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "title": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "user_id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "views": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "profiles": { + "columns": { + "bio": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "user_id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "typed_rows": { + "columns": { + "active": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "created_at": { + "codecId": "sqlite/datetime@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "label": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "metadata": { + "codecId": "sqlite/json@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + }, + "users": { + "columns": { + "email": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": false + }, + "invited_by_id": { + "codecId": "sqlite/integer@1", + "nativeType": "integer", + "nullable": true + }, + "name": { + "codecId": "sqlite/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": ["id"] + }, + "uniques": [] + } + } + }, + "id": "__unbound__" + } + }, + "storageHash": "sha256:713d00a72dcba37a15c259fc3932797150465fca8dfe672130391ce3a16cc973" + }, + "capabilities": { + "sql": { + "foreignKeys": true, + "jsonAgg": true, + "limit": true, + "orderBy": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts new file mode 100644 index 0000000000..0a193a4e57 --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts @@ -0,0 +1,48 @@ +import { expectTypeOf, test } from 'vitest'; +import type { SqliteContractView } from '../src/core/sqlite-contract-view'; +import type { Contract } from './fixtures/sqlite-contract.d'; + +/** + * Emit-then-consume type tests: `Contract` is the real emitted SQLite contract + * from `test/fixtures/sqlite-contract.d.ts`. Assertions check the projected + * view type against the actual emitted shape, not a hand-authored `typeof`. + */ + +type CV = ReturnType>; + +test('cv.table. resolves to the concrete emitted table leaf', () => { + expectTypeOf< + CV['table']['users']['columns']['id']['codecId'] + >().toEqualTypeOf<'sqlite/integer@1'>(); + expectTypeOf< + CV['table']['users']['columns']['email']['codecId'] + >().toEqualTypeOf<'sqlite/text@1'>(); + expectTypeOf().toEqualTypeOf(); +}); + +test('multiple tables are reachable top-level', () => { + expectTypeOf< + CV['table']['posts']['columns']['id']['codecId'] + >().toEqualTypeOf<'sqlite/integer@1'>(); + expectTypeOf< + CV['table']['comments']['columns']['body']['codecId'] + >().toEqualTypeOf<'sqlite/text@1'>(); +}); + +test('a non-existent table name is a compile error', () => { + const cv = null as unknown as CV; + // @ts-expect-error 'nonexistent' is not an emitted table + cv.table.nonexistent; +}); + +test('valueSet slot is present (SQLite emits none, so it is an empty map)', () => { + expectTypeOf().toEqualTypeOf>(); +}); + +test('cv.entries does not contain the built-in table or valueSet keys', () => { + type Entries = CV['entries']; + type HasTable = 'table' extends keyof Entries ? true : false; + type HasValueSet = 'valueSet' extends keyof Entries ? true : false; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); +}); diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts new file mode 100644 index 0000000000..edcb00f8a1 --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts @@ -0,0 +1,62 @@ +import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; +import { describe, expect, it } from 'vitest'; +import { SqliteContractSerializer } from '../src/core/sqlite-contract-serializer'; +import { SqliteContractView } from '../src/core/sqlite-contract-view'; +import type { Contract } from './fixtures/sqlite-contract.d'; +import contractJson from './fixtures/sqlite-contract.json' with { type: 'json' }; + +const contract = new SqliteContractSerializer().deserializeContract(contractJson); + +describe('SqliteContractView', () => { + it('from() returns a view object', () => { + expect(SqliteContractView.from(contract)).toBeDefined(); + }); + + it('cv.table exposes tables from the default namespace', () => { + const cv = SqliteContractView.from(contract); + expect(cv.table.users).toBeDefined(); + expect(cv.table.posts).toBeDefined(); + }); + + it('cv.table. returns the same entity object as the raw contract', () => { + const cv = SqliteContractView.from(contract); + const rawTables = contract.storage.namespaces[UNBOUND_NAMESPACE_ID].entries.table; + expect(cv.table.users).toBe(rawTables?.users); + }); + + it('cv.valueSet is present and empty (SQLite emits no value sets)', () => { + const cv = SqliteContractView.from(contract); + expect(cv.valueSet).toEqual({}); + }); + + it('cv.entries does not contain the built-in table or valueSet keys', () => { + const cv = SqliteContractView.from(contract); + expect(Object.keys(cv.entries)).not.toContain('table'); + expect(Object.keys(cv.entries)).not.toContain('valueSet'); + }); + + it('cv.entries exposes pack-contributed kinds', () => { + // SQLite emits only the built-in `table` kind, so this hand-builds a + // contract with an extra pack-contributed `policy` kind to prove non-built-in + // kinds land under `.entries`. + const fakeEntry = { name: 'test-pack-entity' }; + const contractWithPackKind = { + ...contract, + storage: { + ...contract.storage, + namespaces: { + [UNBOUND_NAMESPACE_ID]: { + ...contract.storage.namespaces[UNBOUND_NAMESPACE_ID], + entries: { + ...contract.storage.namespaces[UNBOUND_NAMESPACE_ID].entries, + policy: { readPolicy: fakeEntry }, + }, + }, + }, + }, + } as unknown as Contract; + + const cv = SqliteContractView.from(contractWithPackKind); + expect((cv.entries as Record)['policy']).toEqual({ readPolicy: fakeEntry }); + }); +}); From 19e7038ed4098cfbb34cef355fa74ffee6b6048e Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 26 Jun 2026 18:50:32 +0200 Subject: [PATCH 02/12] docs(tml-2892): record upgrade-instructions coverage for examples diff 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../upgrades/0.14-to-0.15/instructions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/instructions.md b/skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/instructions.md index 9c9d209bce..c996bdb1b6 100644 --- a/skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/instructions.md +++ b/skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/instructions.md @@ -82,6 +82,18 @@ the primary `db.asServiceRole().sql`/`.orm` surface (plus `asUser`/`asAnon`) is unchanged. No user action. Incidental substrate diff only. --> + + # Upgrade 0.14 → 0.15 No consumer-facing action is required for this transition. From 101a6cd52bc54eb6fa65719521ccf9c6736e8976 Mon Sep 17 00:00:00 2001 From: willbot Date: Sat, 27 Jun 2026 11:35:56 +0200 Subject: [PATCH 03/12] refactor(tml-2892): recast ContractView to a Contract-superset factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `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.`; 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../app/20260513T0505_initial/migration.ts | 6 +- .../migration.ts | 6 +- examples/retail-store/test/migration.test.ts | 5 +- .../framework-components/src/exports/ir.ts | 14 +- .../src/ir/contract-view.ts | 97 +++++- .../test/contract-view.test.ts | 94 +++++- .../mongo-contract/src/contract-view.ts | 71 ++-- .../mongo-contract/src/exports/index.ts | 4 +- .../test/contract-view.test-d.ts | 36 --- .../mongo-contract/test/contract-view.test.ts | 65 ---- packages/2-mongo-family/9-family/package.json | 2 +- .../src/core/ir/mongo-contract-view.ts | 37 +++ .../2-mongo-family/9-family/src/exports/ir.ts | 3 +- .../9-family/test/fixtures/orm-contract.d.ts | 55 ++++ .../9-family/test/fixtures/orm-contract.json | 303 ++++++++++++++++++ .../test/mongo-contract-view.test-d.ts | 45 +++ .../9-family/test/mongo-contract-view.test.ts | 75 +++++ .../9-family/tsconfig.test.json | 15 + .../1-core/contract/src/contract-view.ts | 106 +++--- .../contract/src/exports/contract-view.ts | 5 +- .../src/core/postgres-contract-view.ts | 49 +-- .../3-targets/postgres/src/exports/runtime.ts | 1 - .../test/fixtures/collision-contract.ts | 116 +++++++ .../test/postgres-contract-view.test-d.ts | 77 ++++- .../test/postgres-contract-view.test.ts | 94 ++++-- .../sqlite/src/core/sqlite-contract-view.ts | 46 +-- .../3-targets/sqlite/src/exports/runtime.ts | 1 - .../sqlite/test/fixtures/sqlite-contract.json | 2 + .../test/sqlite-contract-view.test-d.ts | 28 +- .../sqlite/test/sqlite-contract-view.test.ts | 53 ++- 30 files changed, 1220 insertions(+), 291 deletions(-) delete mode 100644 packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts delete mode 100644 packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts create mode 100644 packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts create mode 100644 packages/2-mongo-family/9-family/test/fixtures/orm-contract.d.ts create mode 100644 packages/2-mongo-family/9-family/test/fixtures/orm-contract.json create mode 100644 packages/2-mongo-family/9-family/test/mongo-contract-view.test-d.ts create mode 100644 packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts create mode 100644 packages/2-mongo-family/9-family/tsconfig.test.json create mode 100644 packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts diff --git a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts index 62edd66a8f..be82cf00e0 100755 --- a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts @@ -1,14 +1,14 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; +import { MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createCollection, createIndex } from '@prisma-next/target-mongo/migration'; import type { Contract } from './end-contract'; import endContractJson from './end-contract.json' with { type: 'json' }; -const endContract = new MongoContractSerializer().deserializeContract(endContractJson); +const endContract = MongoContractView.fromJson(endContractJson); -const COLLECTIONS = MongoContractView.from(endContract).collection; +const COLLECTIONS = endContract.collection; const CARTS_VALIDATOR = COLLECTIONS.carts.validator; const EVENTS_VALIDATOR = COLLECTIONS.events.validator; const INVOICES_VALIDATOR = COLLECTIONS.invoices.validator; diff --git a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts index d36a621ef2..0f0c47d89f 100755 --- a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; +import { MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { AggregateCommand, @@ -14,7 +14,7 @@ import { dataTransform, setValidation } from '@prisma-next/target-mongo/migratio import type { Contract } from './end-contract'; import endContractJson from './end-contract.json' with { type: 'json' }; -const endContract = new MongoContractSerializer().deserializeContract(endContractJson); +const endContract = MongoContractView.fromJson(endContractJson); const STORAGE_HASH = endContract.storage.storageHash; @@ -35,7 +35,7 @@ const STORAGE_HASH = endContract.storage.storageHash; // // The validator is sourced from `end-contract.json` so the op stays in sync // with the contract if the chain is ever re-emitted. -const PRODUCTS_VALIDATOR = MongoContractView.from(endContract).collection.products.validator; +const PRODUCTS_VALIDATOR = endContract.collection.products.validator; function existingProductsWithoutStatus(): MongoQueryPlan { return { diff --git a/examples/retail-store/test/migration.test.ts b/examples/retail-store/test/migration.test.ts index 0c6ac5fc17..7c16d38395 100644 --- a/examples/retail-store/test/migration.test.ts +++ b/examples/retail-store/test/migration.test.ts @@ -1,4 +1,4 @@ -import { MongoContractSerializer, MongoContractView } from '@prisma-next/family-mongo/ir'; +import { MongoContractView } from '@prisma-next/family-mongo/ir'; import { timeouts } from '@prisma-next/test-utils'; import { type Db, MongoClient } from 'mongodb'; import { MongoMemoryReplSet } from 'mongodb-memory-server'; @@ -26,8 +26,7 @@ describe('migration', { timeout: timeouts.spinUpMongoMemoryServer }, () => { }, timeouts.spinUpMongoMemoryServer); it('contract contains expected index definitions', () => { - const contract = new MongoContractSerializer().deserializeContract(contractJson); - const collections = MongoContractView.from(contract).collection; + const collections = MongoContractView.fromJson(contractJson).collection; expect(collections.products.indexes).toMatchObject([ { diff --git a/packages/1-framework/1-core/framework-components/src/exports/ir.ts b/packages/1-framework/1-core/framework-components/src/exports/ir.ts index b45c117784..1db489449b 100644 --- a/packages/1-framework/1-core/framework-components/src/exports/ir.ts +++ b/packages/1-framework/1-core/framework-components/src/exports/ir.ts @@ -1,5 +1,15 @@ -export type { DefaultNamespaceEntries, SingleNamespaceView } from '../ir/contract-view'; -export { buildSingleNamespaceView, promoteBuiltinKinds } from '../ir/contract-view'; +export type { + DefaultNamespaceEntries, + NamespaceAccessor, + PromotedNamespaces, + SingleNamespaceView, +} from '../ir/contract-view'; +export { + buildNamespaceAccessor, + buildSingleNamespaceView, + composeContractView, + promoteBuiltinKinds, +} from '../ir/contract-view'; export { domainElementCoordinates } from '../ir/domain'; export type { AnyEntityKindDescriptor, EntityKindDescriptor } from '../ir/entity-kind'; export { hydrateNamespaceEntities } from '../ir/entity-kind'; diff --git a/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts index 81b5c69e09..8950d3bf01 100644 --- a/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts +++ b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts @@ -38,6 +38,36 @@ export type SingleNamespaceView = { }; }; +/** The `entries` shape of one namespace in a storage map. */ +type EntriesOf = TNamespace extends { readonly entries: infer E } ? E : never; + +/** + * The `namespace` accessor: every storage namespace keyed by its raw id, each + * projected to its {@link SingleNamespaceView}. Fully qualified and + * collision-proof — `view.namespace.` reaches any namespace by id even when + * the name collides with a contract envelope field. + */ +export type NamespaceAccessor = { + readonly [Ns in keyof TNamespaces]: SingleNamespaceView< + EntriesOf, + TBuiltinKinds + >; +}; + +/** + * Root-promoted namespaces: each namespace name promoted to a top-level + * accessor, EXCEPT names that collide with a contract envelope field (`keyof + * TContract`) or with the reserved `namespace` key. Those excluded names stay + * reachable only via {@link NamespaceAccessor}. The key-remapping `as ... ? + * never : Ns` is what keeps the root promotion from shadowing a contract field + * at the type level. + */ +export type PromotedNamespaces = { + readonly [Ns in keyof TNamespaces as Ns extends keyof TContract | 'namespace' + ? never + : Ns]: SingleNamespaceView, TBuiltinKinds>; +}; + /** * Projects one namespace's `entries` into the view shape: each built-in kind * becomes a top-level slot (materialized empty if absent), and the remaining @@ -69,7 +99,7 @@ export function promoteBuiltinKinds( } /** - * Builds the runtime projection object for a single-namespace contract: unwraps + * Builds the runtime accessor object for a single-namespace contract: unwraps * the default namespace and promotes the given built-in kind slots to top-level. * The static type is supplied by the caller via the generic factory; this * function is structural. @@ -90,3 +120,68 @@ export function buildSingleNamespaceView( >(defaultNs.entries); return promoteBuiltinKinds(entries, builtinKinds); } + +/** + * Builds the per-namespace accessor map (`{ : SingleNamespaceView }`) for + * every namespace in the storage, keyed by raw namespace id. + */ +export function buildNamespaceAccessor( + storage: Storage, + builtinKinds: readonly string[], +): TAccessor { + const out: Record = {}; + for (const [nsId, ns] of Object.entries(storage.namespaces)) { + out[nsId] = promoteBuiltinKinds( + blindCast< + Readonly>, + 'Namespace.entries is the open ADR 224 dictionary Record>' + >(ns.entries), + builtinKinds, + ); + } + return blindCast< + TAccessor, + 'each namespace projected to its SingleNamespaceView; keys mirror the storage namespace ids' + >(out); +} + +/** + * Composes a contract view from the deserialized contract plus its accessors, + * enforcing collision-safe layering so the result stays substitutable for the + * contract: + * + * 1. **Contract envelope fields win at the root.** Every contract own field + * (`storage`, `domain`, `roots`, …) is copied first and is never overwritten. + * 2. **`rootAccessors`** (kind-promoted slots for the single-namespace families, + * e.g. `collection`/`table`/`entries`) are layered next; for Postgres this is + * empty. + * 3. **`namespace`** holds every namespace by id — the fully-qualified, + * collision-proof accessor. + * 4. **Root-promoted namespace names** are added last, but ONLY for names not + * already present at the root (not a contract field, not `namespace`, not an + * already-promoted kind slot). A namespace named `storage` is therefore + * reachable only via `view.namespace.storage`, while `view.storage` stays the + * contract's storage. + */ +export function composeContractView( + contract: object, + rootAccessors: Readonly>, + namespaceAccessor: Readonly>, +): TView { + const root: Record = { ...contract }; + for (const [key, value] of Object.entries(rootAccessors)) { + if (!(key in root)) { + root[key] = value; + } + } + root['namespace'] = namespaceAccessor; + for (const [nsId, nsView] of Object.entries(namespaceAccessor)) { + if (!(nsId in root)) { + root[nsId] = nsView; + } + } + return blindCast< + TView, + 'root carries every contract field, the kind-promoted root accessors, the namespace map, and the non-colliding promoted namespace names; the view type is the caller-supplied intersection' + >(root); +} diff --git a/packages/1-framework/1-core/framework-components/test/contract-view.test.ts b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts index c9edafd08e..59f46de819 100644 --- a/packages/1-framework/1-core/framework-components/test/contract-view.test.ts +++ b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { buildSingleNamespaceView } from '../src/ir/contract-view'; +import { + buildNamespaceAccessor, + buildSingleNamespaceView, + composeContractView, +} from '../src/ir/contract-view'; import { UNBOUND_NAMESPACE_ID } from '../src/ir/namespace'; import type { Storage } from '../src/ir/storage'; @@ -11,6 +15,17 @@ function storageWith(entries: Record): Storage { } as unknown as Storage; } +function multiNamespaceStorage(namespaces: Record>): Storage { + return { + namespaces: Object.fromEntries( + Object.entries(namespaces).map(([id, entries]) => [ + id, + { id, kind: 'test-namespace', entries }, + ]), + ), + } as unknown as Storage; +} + describe('buildSingleNamespaceView', () => { it('promotes built-in kinds to top-level and keeps pack kinds under .entries', () => { const table = { users: { name: 'users' } }; @@ -51,3 +66,80 @@ describe('buildSingleNamespaceView', () => { expect(() => buildSingleNamespaceView(storage, ['table'])).toThrow(/default namespace/); }); }); + +describe('buildNamespaceAccessor', () => { + it('keys every namespace by raw id, each kind-promoted', () => { + const storage = multiNamespaceStorage({ + public: { table: { users: { c: 1 } } }, + auth: { table: { sessions: { c: 2 } } }, + }); + const ns = buildNamespaceAccessor<{ + public: { table: { users: unknown }; entries: object }; + auth: { table: { sessions: unknown }; entries: object }; + }>(storage, ['table', 'valueSet']); + + expect(Object.keys(ns).sort()).toEqual(['auth', 'public']); + expect(ns.public.table.users).toEqual({ c: 1 }); + expect(ns.auth.table.sessions).toEqual({ c: 2 }); + }); +}); + +describe('composeContractView', () => { + it('contract fields win at the root; root accessors and namespace are added', () => { + const contract = { storage: { ns: 1 }, domain: { models: {} }, roots: {} }; + const rootAccessors = { table: { users: {} }, entries: {} }; + const namespaceAccessor = { __unbound__: { table: { users: {} }, entries: {} } }; + const view = composeContractView< + typeof contract & typeof rootAccessors & { namespace: typeof namespaceAccessor } + >(contract, rootAccessors, namespaceAccessor); + + expect(view.storage).toBe(contract.storage); + expect(view.table).toBe(rootAccessors.table); + expect(view.namespace).toBe(namespaceAccessor); + expect(view).not.toBe(contract); + }); + + it('promotes non-colliding namespace names to the root', () => { + const contract = { storage: {}, domain: {}, roots: {} }; + const namespaceAccessor = { + public: { table: { widgets: {} } }, + auth: { table: { sessions: {} } }, + }; + const view = composeContractView>(contract, {}, namespaceAccessor); + + expect(view['public']).toBe(namespaceAccessor.public); + expect(view['auth']).toBe(namespaceAccessor.auth); + }); + + it('does NOT promote a namespace whose name collides with a contract field', () => { + const contract = { storage: { isContractField: true }, domain: {}, roots: {} }; + const namespaceAccessor = { + storage: { table: { secrets: {} } }, + public: { table: { widgets: {} } }, + }; + const view = composeContractView>(contract, {}, namespaceAccessor); + + // The contract field wins at the root. + expect(view['storage']).toBe(contract.storage); + expect((view['storage'] as Record)['table']).toBeUndefined(); + // The colliding schema is reachable only via the namespace accessor. + expect((view['namespace'] as Record)['storage']).toBe( + namespaceAccessor.storage, + ); + // A non-colliding schema is still promoted. + expect(view['public']).toBe(namespaceAccessor.public); + }); + + it('does NOT let a namespace named `namespace` overwrite the namespace accessor', () => { + const contract = { storage: {}, domain: {}, roots: {} }; + const namespaceAccessor = { namespace: { table: { x: {} } }, public: { table: { y: {} } } }; + const view = composeContractView>(contract, {}, namespaceAccessor); + + // `view.namespace` is the accessor map (holds both ids), not the schema view. + expect(view['namespace']).toBe(namespaceAccessor); + expect((view['namespace'] as Record)['namespace']).toBe( + namespaceAccessor.namespace, + ); + expect(view['public']).toBe(namespaceAccessor.public); + }); +}); diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts index 60eba70913..d75b227c54 100644 --- a/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts +++ b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts @@ -1,6 +1,9 @@ import { + buildNamespaceAccessor, buildSingleNamespaceView, + composeContractView, type DefaultNamespaceEntries, + type NamespaceAccessor, type SingleNamespaceView, } from '@prisma-next/framework-components/ir'; import type { MongoContract } from './contract-types'; @@ -10,38 +13,54 @@ type MongoBuiltinKind = (typeof MONGO_BUILTIN_KINDS)[number]; type MongoEntries = DefaultNamespaceEntries; -export type MongoContractViewShape = SingleNamespaceView< +type MongoNamespaces = TContract['storage']['namespaces']; + +/** + * The Mongo accessors: the built-in `collection` kind promoted to a top-level + * accessor, pack-contributed kinds under `entries` (singular keys). + */ +export type MongoContractAccessors = SingleNamespaceView< MongoEntries, MongoBuiltinKind >; /** - * A read-only view over a deserialized Mongo contract that unwraps the - * default namespace and promotes the built-in `collection` kind to the - * top level. + * A Mongo contract view: the deserialized contract intersected with the by-name + * accessors, so the value is substitutable for `Contract` (carries `storage`, + * `domain`, …) while also exposing: + * - `view.collection.` — the built-in kind, default namespace unwrapped. + * - `view.entries.` — pack-contributed kinds (singular keys). + * - `view.namespace.` — every namespace by raw id (Mongo's sole namespace + * is `__unbound__`), the fully-qualified collision-proof accessor. * - * Usage: - * ```ts - * const cv = MongoContractView.from(endContract); - * cv.collection.carts // typed MongoCollection - * cv.entries.policy.X // pack-contributed kind (singular key) - * ``` - * - * The `Contract` type is unchanged — this view is a separate object layered - * on top of the raw deserialized contract. The default-namespace unwrap and - * built-in-kind promotion are the generic single-namespace projection from - * `@prisma-next/framework-components`; Mongo only supplies its built-in kind - * set (`collection`). + * The factory (`MongoContractView.from` / `.fromJson`) lives in + * `@prisma-next/family-mongo/ir`, where the Mongo serializer is reachable; this + * package owns the serializer-agnostic projection type and builder. */ -export class MongoContractView { - private constructor() {} +export type MongoContractView = TContract & + MongoContractAccessors & { + readonly namespace: NamespaceAccessor, MongoBuiltinKind>; + }; - static from( - contract: TContract, - ): MongoContractViewShape { - return buildSingleNamespaceView>( - contract.storage, - MONGO_BUILTIN_KINDS, - ); - } +/** + * Builds the Mongo view: unwraps the default namespace, promotes the built-in + * `collection` kind at the root, attaches the `namespace` accessor, and layers + * everything over the deserialized contract so the result is a structural + * superset of the contract. + */ +export function buildMongoContractView( + contract: TContract, +): MongoContractView { + const rootAccessors = buildSingleNamespaceView>( + contract.storage, + MONGO_BUILTIN_KINDS, + ); + const namespaceAccessor = buildNamespaceAccessor< + NamespaceAccessor, MongoBuiltinKind> + >(contract.storage, MONGO_BUILTIN_KINDS); + return composeContractView>( + contract, + rootAccessors, + namespaceAccessor, + ); } diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts b/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts index e6c66cf936..9c63bf700e 100644 --- a/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts +++ b/packages/2-mongo-family/1-foundation/mongo-contract/src/exports/index.ts @@ -31,8 +31,8 @@ export type { MongoWildcardProjection, RootModelName, } from '../contract-types'; -export type { MongoContractViewShape } from '../contract-view'; -export { MongoContractView } from '../contract-view'; +export type { MongoContractAccessors, MongoContractView } from '../contract-view'; +export { buildMongoContractView } from '../contract-view'; export { defaultMongoDomainNamespaceId, defaultMongoStorageNamespaceId, diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts deleted file mode 100644 index a5b0c3c2aa..0000000000 --- a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test-d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { expectTypeOf, test } from 'vitest'; -import type { MongoContractView } from '../src/contract-view'; -import type { MongoCollection } from '../src/ir/mongo-collection'; -import type { Contract } from './fixtures/orm-contract.d'; - -/** - * Emit-then-consume type tests: the `Contract` type is the real emitted - * contract from `test/fixtures/orm-contract.d.ts`. All assertions check the - * projected type against the actual emitted shape, not a hand-authored - * `typeof` expression. - */ - -test('MongoContractView.from returns a MongoContractView', () => { - type CV = ReturnType>; - expectTypeOf().toMatchTypeOf<{ collection: object; entries: object }>(); -}); - -test('cv.collection gives correctly typed built-in collection entities', () => { - type CV = ReturnType>; - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); -}); - -test('a non-existent collection name is a compile error', () => { - type CV = ReturnType>; - const cv = null as unknown as CV; - // @ts-expect-error 'nonexistent' does not exist on the collection map - cv.collection.nonexistent; -}); - -test('cv.entries does not contain the collection key', () => { - type CV = ReturnType>; - type Entries = CV['entries']; - type HasCollection = 'collection' extends keyof Entries ? true : false; - expectTypeOf().toEqualTypeOf(); -}); diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts b/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts deleted file mode 100644 index 0f5126c1c6..0000000000 --- a/packages/2-mongo-family/1-foundation/mongo-contract/test/contract-view.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { MongoContractView } from '../src/contract-view'; -import type { Contract } from './fixtures/orm-contract.d'; -import contractJson from './fixtures/orm-contract.json' with { type: 'json' }; - -// The fixture JSON is typed as the emitted `Contract` so the view's projected -// types are exercised. These tests assert runtime property access on the view, -// not contract deserialization. -const contract = contractJson as unknown as Contract; - -describe('MongoContractView', () => { - it('from() returns a view object', () => { - const cv = MongoContractView.from(contract); - expect(cv).toBeDefined(); - }); - - it('cv.collection exposes collections from the default namespace', () => { - const cv = MongoContractView.from(contract); - expect(cv.collection).toBeDefined(); - expect(cv.collection.tasks).toBeDefined(); - expect(cv.collection.users).toBeDefined(); - }); - - it('cv.collection. returns the MongoCollection entity', () => { - const cv = MongoContractView.from(contract); - expect(cv.collection.tasks).toBe( - contract.storage.namespaces['__unbound__'].entries['collection']['tasks'], - ); - expect(cv.collection.users).toBe( - contract.storage.namespaces['__unbound__'].entries['collection']['users'], - ); - }); - - it('cv.entries does not contain the collection key', () => { - const cv = MongoContractView.from(contract); - expect(Object.keys(cv.entries)).not.toContain('collection'); - }); - - it('cv.entries exposes pack-contributed kinds', () => { - // The fixture only carries the built-in `collection` kind, so this test - // hand-builds a contract with an extra pack-contributed `policy` kind to - // prove non-built-in kinds land under `.entries`. - const fakeEntry = { name: 'test-pack-entity' }; - const contractWithPackKind = { - ...contract, - storage: { - ...contract.storage, - namespaces: { - __unbound__: { - ...contract.storage.namespaces['__unbound__'], - entries: { - ...contract.storage.namespaces['__unbound__'].entries, - policy: { readPolicy: fakeEntry }, - }, - }, - }, - }, - } as unknown as Contract; - - const cv = MongoContractView.from(contractWithPackKind); - expect((cv.entries as Record)['policy']).toEqual({ - readPolicy: fakeEntry, - }); - }); -}); diff --git a/packages/2-mongo-family/9-family/package.json b/packages/2-mongo-family/9-family/package.json index 22830aa02c..abc58f3068 100644 --- a/packages/2-mongo-family/9-family/package.json +++ b/packages/2-mongo-family/9-family/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "tsdown", "test": "vitest run --passWithNoTests", - "typecheck": "tsc --project tsconfig.json --noEmit", + "typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit", "lint": "biome check . --error-on-warnings", "clean": "rm -rf dist coverage" }, diff --git a/packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts b/packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts new file mode 100644 index 0000000000..d6d01a8e80 --- /dev/null +++ b/packages/2-mongo-family/9-family/src/core/ir/mongo-contract-view.ts @@ -0,0 +1,37 @@ +import { + buildMongoContractView, + type MongoContract, + type MongoContractView as MongoContractViewType, +} from '@prisma-next/mongo-contract'; +import { MongoContractSerializer } from './mongo-contract-serializer'; + +/** + * A Mongo contract view: the deserialized contract intersected with by-name + * accessors. It is substitutable for `Contract` (carries `storage`, `domain`, + * …) and also exposes the single default namespace unwrapped with the built-in + * `collection` kind promoted: + * + * ```ts + * const view = MongoContractView.fromJson(contractJson); + * view.collection.carts.validator // typed MongoCollection + * view.entries.policy.X // pack-contributed kind (singular key) + * view.storage // the full contract is still present + * ``` + */ +export type MongoContractView = + MongoContractViewType; + +export const MongoContractView = { + /** Wrap an already-deserialized Mongo contract in a view. */ + from(contract: TContract): MongoContractView { + return buildMongoContractView(contract); + }, + + /** Deserialize a Mongo contract JSON envelope and wrap it in a view. */ + fromJson( + json: unknown, + ): MongoContractView { + const contract = new MongoContractSerializer().deserializeContract(json); + return buildMongoContractView(contract); + }, +}; diff --git a/packages/2-mongo-family/9-family/src/exports/ir.ts b/packages/2-mongo-family/9-family/src/exports/ir.ts index 0a1cd44a71..5ea64abae9 100644 --- a/packages/2-mongo-family/9-family/src/exports/ir.ts +++ b/packages/2-mongo-family/9-family/src/exports/ir.ts @@ -1,5 +1,4 @@ -export type { MongoContractViewShape } from '@prisma-next/mongo-contract'; -export { MongoContractView } from '@prisma-next/mongo-contract'; export { MongoContractSerializer } from '../core/ir/mongo-contract-serializer'; export { MongoContractSerializerBase } from '../core/ir/mongo-contract-serializer-base'; +export { MongoContractView } from '../core/ir/mongo-contract-view'; export { MongoSchemaVerifierBase } from '../core/ir/mongo-schema-verifier-base'; diff --git a/packages/2-mongo-family/9-family/test/fixtures/orm-contract.d.ts b/packages/2-mongo-family/9-family/test/fixtures/orm-contract.d.ts new file mode 100644 index 0000000000..ca8aa9458c --- /dev/null +++ b/packages/2-mongo-family/9-family/test/fixtures/orm-contract.d.ts @@ -0,0 +1,55 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { CodecTypes as MongoCodecTypes } from '@prisma-next/adapter-mongo/codec-types'; + +import type { + MongoCollection, + MongoContractWithTypeMaps, + MongoTypeMaps, +} from '@prisma-next/mongo-contract'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = StorageHashBase<'undefined'>; +export type ExecutionHash = ExecutionHashBase; +export type ProfileHash = ProfileHashBase<'undefined'>; + +export type CodecTypes = MongoCodecTypes; + +export type HomeAddressOutput = { readonly city: CodecTypes['mongo/string@1']['output']; readonly country: CodecTypes['mongo/string@1']['output'] }; +export type HomeAddressInput = { readonly city: CodecTypes['mongo/string@1']['input']; readonly country: CodecTypes['mongo/string@1']['input'] }; +export type FieldOutputTypes = { readonly __unbound__: { readonly Address: { readonly street: CodecTypes['mongo/string@1']['output']; readonly city: CodecTypes['mongo/string@1']['output']; readonly zip: CodecTypes['mongo/string@1']['output'] }; readonly Bug: { readonly severity: CodecTypes['mongo/string@1']['output'] }; readonly Comment: { readonly _id: CodecTypes['mongo/objectId@1']['output']; readonly text: CodecTypes['mongo/string@1']['output']; readonly createdAt: CodecTypes['mongo/date@1']['output'] }; readonly Feature: { readonly priority: CodecTypes['mongo/string@1']['output']; readonly targetRelease: CodecTypes['mongo/string@1']['output'] }; readonly Task: { readonly _id: CodecTypes['mongo/objectId@1']['output']; readonly title: CodecTypes['mongo/string@1']['output']; readonly type: CodecTypes['mongo/string@1']['output']; readonly assigneeId: CodecTypes['mongo/objectId@1']['output'] }; readonly User: { readonly _id: CodecTypes['mongo/objectId@1']['output']; readonly name: CodecTypes['mongo/string@1']['output']; readonly email: CodecTypes['mongo/string@1']['output']; readonly loginCount: CodecTypes['mongo/int32@1']['output']; readonly tags: ReadonlyArray; readonly homeAddress: HomeAddressOutput | null } } }; +export type FieldInputTypes = { readonly __unbound__: { readonly Address: { readonly street: CodecTypes['mongo/string@1']['input']; readonly city: CodecTypes['mongo/string@1']['input']; readonly zip: CodecTypes['mongo/string@1']['input'] }; readonly Bug: { readonly severity: CodecTypes['mongo/string@1']['input'] }; readonly Comment: { readonly _id: CodecTypes['mongo/objectId@1']['input']; readonly text: CodecTypes['mongo/string@1']['input']; readonly createdAt: CodecTypes['mongo/date@1']['input'] }; readonly Feature: { readonly priority: CodecTypes['mongo/string@1']['input']; readonly targetRelease: CodecTypes['mongo/string@1']['input'] }; readonly Task: { readonly _id: CodecTypes['mongo/objectId@1']['input']; readonly title: CodecTypes['mongo/string@1']['input']; readonly type: CodecTypes['mongo/string@1']['input']; readonly assigneeId: CodecTypes['mongo/objectId@1']['input'] }; readonly User: { readonly _id: CodecTypes['mongo/objectId@1']['input']; readonly name: CodecTypes['mongo/string@1']['input']; readonly email: CodecTypes['mongo/string@1']['input']; readonly loginCount: CodecTypes['mongo/int32@1']['input']; readonly tags: ReadonlyArray; readonly homeAddress: HomeAddressInput | null } } }; +export type TypeMaps = MongoTypeMaps; + +type ContractBase = Omit< + ContractType< +{ readonly namespaces: { readonly __unbound__: { readonly id: '__unbound__'; readonly kind: 'mongo-namespace'; readonly entries: { readonly collection: { readonly tasks: MongoCollection; readonly users: MongoCollection } } } }; readonly storageHash: StorageHash }>, + 'roots' | 'domain' +> & { + readonly target: 'mongo'; + readonly targetFamily: 'mongo'; + readonly roots: { readonly tasks: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Task' }; readonly users: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' } }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { readonly Address: { readonly fields: { readonly street: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly city: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly zip: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } } }; readonly relations: Record; readonly storage: Record; readonly owner: 'User' }; readonly Bug: { readonly fields: { readonly severity: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } } }; readonly relations: Record; readonly storage: { readonly collection: 'tasks' }; readonly base: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Task' } }; readonly Comment: { readonly fields: { readonly _id: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' } }; readonly text: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly createdAt: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' } } }; readonly relations: Record; readonly storage: Record; readonly owner: 'Task' }; readonly Feature: { readonly fields: { readonly priority: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly targetRelease: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } } }; readonly relations: Record; readonly storage: { readonly collection: 'tasks' }; readonly base: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Task' } }; readonly Task: { readonly fields: { readonly _id: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' } }; readonly title: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly type: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly assigneeId: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' } } }; readonly relations: { readonly assignee: { readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; readonly cardinality: 'N:1'; readonly on: { readonly localFields: readonly ['assigneeId']; readonly targetFields: readonly ['_id'] } }; readonly comments: { readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Comment' }; readonly cardinality: '1:N' } }; readonly storage: { readonly collection: 'tasks'; readonly relations: { readonly comments: { readonly field: 'comments' } } }; readonly discriminator: { readonly field: 'type' }; readonly variants: { readonly Bug: { readonly value: 'bug' }; readonly Feature: { readonly value: 'feature' } } }; readonly User: { readonly fields: { readonly _id: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' } }; readonly name: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly email: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly loginCount: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' } }; readonly tags: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; readonly many: true }; readonly homeAddress: { readonly nullable: true; readonly type: { readonly kind: 'valueObject'; readonly name: 'HomeAddress' } } }; readonly relations: { readonly addresses: { readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Address' }; readonly cardinality: '1:N' }; readonly tasks: { readonly to: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Task' }; readonly cardinality: '1:N'; readonly on: { readonly localFields: readonly ['_id']; readonly targetFields: readonly ['assigneeId'] } } }; readonly storage: { readonly collection: 'users'; readonly relations: { readonly addresses: { readonly field: 'addresses' } } } } }; + readonly valueObjects: { readonly HomeAddress: { readonly fields: { readonly city: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly country: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } } } } }; + }; + }; + }; + readonly capabilities: { }; + readonly extensionPacks: { }; + readonly meta: { }; + readonly valueObjects: { readonly HomeAddress: { readonly fields: { readonly city: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } }; readonly country: { readonly nullable: false; readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' } } } } }; + readonly profileHash: ProfileHash; +}; + + +export type Contract = MongoContractWithTypeMaps; diff --git a/packages/2-mongo-family/9-family/test/fixtures/orm-contract.json b/packages/2-mongo-family/9-family/test/fixtures/orm-contract.json new file mode 100644 index 0000000000..d0f1c2be98 --- /dev/null +++ b/packages/2-mongo-family/9-family/test/fixtures/orm-contract.json @@ -0,0 +1,303 @@ +{ + "targetFamily": "mongo", + "storageHash": "fixture-hash", + "roots": { + "tasks": { + "model": "Task", + "namespace": "__unbound__" + }, + "users": { + "model": "User", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "entries": { + "collection": { + "tasks": {}, + "users": {} + } + } + } + } + }, + "target": "mongo", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "Task": { + "storage": { + "collection": "tasks", + "relations": { + "comments": { + "field": "comments" + } + } + }, + "fields": { + "_id": { + "type": { + "kind": "scalar", + "codecId": "mongo/objectId@1" + }, + "nullable": false + }, + "title": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "type": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "assigneeId": { + "type": { + "kind": "scalar", + "codecId": "mongo/objectId@1" + }, + "nullable": false + } + }, + "relations": { + "assignee": { + "to": { + "model": "User", + "namespace": "__unbound__" + }, + "cardinality": "N:1", + "on": { + "localFields": ["assigneeId"], + "targetFields": ["_id"] + } + }, + "comments": { + "to": { + "model": "Comment", + "namespace": "__unbound__" + }, + "cardinality": "1:N" + } + }, + "discriminator": { + "field": "type" + }, + "variants": { + "Bug": { + "value": "bug" + }, + "Feature": { + "value": "feature" + } + } + }, + "Bug": { + "storage": { + "collection": "tasks" + }, + "fields": { + "severity": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + } + }, + "relations": {}, + "base": { + "model": "Task", + "namespace": "__unbound__" + } + }, + "Feature": { + "storage": { + "collection": "tasks" + }, + "fields": { + "priority": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "targetRelease": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + } + }, + "relations": {}, + "base": { + "model": "Task", + "namespace": "__unbound__" + } + }, + "User": { + "storage": { + "collection": "users", + "relations": { + "addresses": { + "field": "addresses" + } + } + }, + "fields": { + "_id": { + "type": { + "kind": "scalar", + "codecId": "mongo/objectId@1" + }, + "nullable": false + }, + "name": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "email": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "loginCount": { + "type": { + "kind": "scalar", + "codecId": "mongo/int32@1" + }, + "nullable": false + }, + "tags": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false, + "many": true + }, + "homeAddress": { + "type": { + "kind": "valueObject", + "name": "HomeAddress" + }, + "nullable": true + } + }, + "relations": { + "addresses": { + "to": { + "model": "Address", + "namespace": "__unbound__" + }, + "cardinality": "1:N" + }, + "tasks": { + "to": { + "model": "Task", + "namespace": "__unbound__" + }, + "cardinality": "1:N", + "on": { + "localFields": ["_id"], + "targetFields": ["assigneeId"] + } + } + } + }, + "Address": { + "storage": {}, + "fields": { + "street": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "city": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "zip": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + } + }, + "relations": {}, + "owner": "User" + }, + "Comment": { + "storage": {}, + "fields": { + "_id": { + "type": { + "kind": "scalar", + "codecId": "mongo/objectId@1" + }, + "nullable": false + }, + "text": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "createdAt": { + "type": { + "kind": "scalar", + "codecId": "mongo/date@1" + }, + "nullable": false + } + }, + "relations": {}, + "owner": "Task" + } + }, + "valueObjects": { + "HomeAddress": { + "fields": { + "city": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + }, + "country": { + "type": { + "kind": "scalar", + "codecId": "mongo/string@1" + }, + "nullable": false + } + } + } + } + } + } + } +} diff --git a/packages/2-mongo-family/9-family/test/mongo-contract-view.test-d.ts b/packages/2-mongo-family/9-family/test/mongo-contract-view.test-d.ts new file mode 100644 index 0000000000..5f68d192f5 --- /dev/null +++ b/packages/2-mongo-family/9-family/test/mongo-contract-view.test-d.ts @@ -0,0 +1,45 @@ +import type { Contract as ContractBase } from '@prisma-next/contract/types'; +import type { MongoCollection } from '@prisma-next/mongo-contract'; +import { expectTypeOf, test } from 'vitest'; +import { MongoContractView } from '../src/core/ir/mongo-contract-view'; +import type { Contract } from './fixtures/orm-contract.d'; + +/** + * Emit-then-consume type tests: the `Contract` type is the real emitted + * contract from `test/fixtures/orm-contract.d.ts`. All assertions check the + * projected type against the actual emitted shape, not a hand-authored `typeof`. + */ + +type CV = MongoContractView; + +test('the view is assignable to Contract (superset)', () => { + expectTypeOf().toMatchTypeOf(); +}); + +test('from() and fromJson() both return the view type', () => { + expectTypeOf(MongoContractView.from).returns.toEqualTypeOf(); + expectTypeOf(MongoContractView.fromJson).returns.toEqualTypeOf(); +}); + +test('view.collection gives correctly typed built-in collection entities', () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); +}); + +test('view.namespace.__unbound__ reaches the default namespace by id', () => { + expectTypeOf< + CV['namespace']['__unbound__']['collection']['tasks'] + >().toEqualTypeOf(); +}); + +test('a non-existent collection name is a compile error', () => { + const view = null as unknown as CV; + // @ts-expect-error 'nonexistent' does not exist on the collection map + view.collection.nonexistent; +}); + +test('view.entries does not contain the collection key', () => { + type Entries = CV['entries']; + type HasCollection = 'collection' extends keyof Entries ? true : false; + expectTypeOf().toEqualTypeOf(); +}); diff --git a/packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts b/packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts new file mode 100644 index 0000000000..ec12c34fe5 --- /dev/null +++ b/packages/2-mongo-family/9-family/test/mongo-contract-view.test.ts @@ -0,0 +1,75 @@ +import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; +import { describe, expect, it } from 'vitest'; +import { MongoContractSerializer } from '../src/core/ir/mongo-contract-serializer'; +import { MongoContractView } from '../src/core/ir/mongo-contract-view'; +import type { Contract } from './fixtures/orm-contract.d'; +import contractJson from './fixtures/orm-contract.json' with { type: 'json' }; + +const contract = new MongoContractSerializer().deserializeContract(contractJson); + +describe('MongoContractView', () => { + it('from() returns a view object', () => { + expect(MongoContractView.from(contract)).toBeDefined(); + }); + + it('the view is a superset of the contract (contract fields present)', () => { + const view = MongoContractView.from(contract); + expect(view.storage).toBe(contract.storage); + expect(view.domain).toBe(contract.domain); + expect(view.roots).toBe(contract.roots); + }); + + it('view.collection exposes collections from the default namespace', () => { + const view = MongoContractView.from(contract); + expect(view.collection.tasks).toBeDefined(); + expect(view.collection.users).toBeDefined(); + }); + + it('view.collection. returns the same entity object as the raw contract', () => { + const view = MongoContractView.from(contract); + const rawCollections = contract.storage.namespaces[UNBOUND_NAMESPACE_ID].entries['collection']; + expect(view.collection.tasks).toBe(rawCollections['tasks']); + expect(view.collection.users).toBe(rawCollections['users']); + }); + + it('view.namespace.__unbound__ reaches the default namespace by id', () => { + const view = MongoContractView.from(contract); + expect(view.namespace[UNBOUND_NAMESPACE_ID].collection.tasks).toBe(view.collection.tasks); + }); + + it('view.entries does not contain the collection key', () => { + const view = MongoContractView.from(contract); + expect(Object.keys(view.entries)).not.toContain('collection'); + }); + + it('fromJson() deserializes and wraps in one call', () => { + const view = MongoContractView.fromJson(contractJson); + expect(view.collection.tasks).toBeDefined(); + expect(view.storage.storageHash).toBe(contract.storage.storageHash); + }); + + it('view.entries exposes pack-contributed kinds', () => { + // The fixture carries only the built-in `collection` kind, so this + // hand-builds a contract with an extra pack-contributed `policy` kind to + // prove non-built-in kinds land under `.entries`. + const fakeEntry = { name: 'test-pack-entity' }; + const contractWithPackKind = { + ...contract, + storage: { + ...contract.storage, + namespaces: { + [UNBOUND_NAMESPACE_ID]: { + ...contract.storage.namespaces[UNBOUND_NAMESPACE_ID], + entries: { + ...contract.storage.namespaces[UNBOUND_NAMESPACE_ID].entries, + policy: { readPolicy: fakeEntry }, + }, + }, + }, + }, + } as unknown as Contract; + + const view = MongoContractView.from(contractWithPackKind); + expect((view.entries as Record)['policy']).toEqual({ readPolicy: fakeEntry }); + }); +}); diff --git a/packages/2-mongo-family/9-family/tsconfig.test.json b/packages/2-mongo-family/9-family/tsconfig.test.json new file mode 100644 index 0000000000..c6d45b4073 --- /dev/null +++ b/packages/2-mongo-family/9-family/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": ["@prisma-next/tsconfig/base"], + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "src/**/*.ts", + "test/mongo-contract-view.test.ts", + "test/mongo-contract-view.test-d.ts" + ], + "exclude": ["dist"] +} diff --git a/packages/2-sql/1-core/contract/src/contract-view.ts b/packages/2-sql/1-core/contract/src/contract-view.ts index b010e9cd7e..8a694dcb24 100644 --- a/packages/2-sql/1-core/contract/src/contract-view.ts +++ b/packages/2-sql/1-core/contract/src/contract-view.ts @@ -1,11 +1,13 @@ import type { Contract } from '@prisma-next/contract/types'; import { + buildNamespaceAccessor, buildSingleNamespaceView, + composeContractView, type DefaultNamespaceEntries, - promoteBuiltinKinds, + type NamespaceAccessor, + type PromotedNamespaces, type SingleNamespaceView, } from '@prisma-next/framework-components/ir'; -import { blindCast } from '@prisma-next/utils/casts'; import type { SqlStorage } from './ir/sql-storage'; /** @@ -20,67 +22,83 @@ type SqlEntries> = DefaultNamespaceEntrie TContract['storage'] >; +type SqlNamespaces> = TContract['storage']['namespaces']; + /** - * Single-namespace SQL view shape: `cv.table.` and `cv.valueSet.` - * top-level, pack kinds under `cv.entries.`. A target that never emits a - * given built-in kind (SQLite has `sql.enums: false`, so it emits no - * `valueSet`) resolves that slot to an empty map. + * The single-namespace SQL accessors: `table`/`valueSet` top-level, pack kinds + * under `entries`. A target that never emits a built-in kind (SQLite has + * `sql.enums: false`, so it emits no `valueSet`) resolves that slot to an empty + * map. */ -export type SqlSingleNamespaceViewShape> = +export type SqlSingleNamespaceAccessors> = SingleNamespaceView, SqlBuiltinKind>; /** - * Builds the single-namespace SQL view: unwraps the default namespace and - * promotes the SQL built-in kinds (`table`, `valueSet`). Targets with one - * default namespace (SQLite) call this directly; Postgres qualifies by schema - * and does not use it. + * Single-namespace SQL view: the deserialized contract intersected with the + * by-name accessors, so the value is substitutable for `Contract` while also + * exposing: + * - `view.table.` / `view.valueSet.` — built-in kinds, default + * namespace unwrapped; pack kinds under `view.entries.`. + * - `view.namespace.` — every namespace by raw id (SQLite's sole namespace + * is `__unbound__`). + */ +export type SqlSingleNamespaceView> = TContract & + SqlSingleNamespaceAccessors & { + readonly namespace: NamespaceAccessor, SqlBuiltinKind>; + }; + +/** + * Builds the single-namespace SQL view: promotes the SQL built-in-kind accessors + * (`table`, `valueSet`) at the root, attaches the `namespace` accessor, and + * layers everything over the deserialized contract. Targets with one default + * namespace (SQLite) call this directly; Postgres qualifies by schema. */ export function buildSqlSingleNamespaceView>( contract: TContract, -): SqlSingleNamespaceViewShape { - return buildSingleNamespaceView>( +): SqlSingleNamespaceView { + const rootAccessors = buildSingleNamespaceView>( contract.storage, SQL_BUILTIN_KINDS, ); + const namespaceAccessor = buildNamespaceAccessor< + NamespaceAccessor, SqlBuiltinKind> + >(contract.storage, SQL_BUILTIN_KINDS); + return composeContractView>( + contract, + rootAccessors, + namespaceAccessor, + ); } /** - * Schema-qualified SQL view shape: each storage namespace key (`public`, - * `auth`, the default `__unbound__`, …) maps to its own single-namespace view, - * mirroring the facade's `sql..
` keying exactly — including the - * literal `__unbound__` key for the default schema (the facade does not rename - * it). Within each schema, `cv..table.` / `cv..valueSet.` - * are top-level and pack kinds sit under `cv..entries.`. + * Schema-qualified SQL view: the deserialized contract intersected with + * - `view.namespace.` — every schema by raw id (the fully-qualified, + * collision-proof accessor; `view.namespace.storage` is the schema literally + * named `storage`), and + * - root-promoted schema names — each schema promoted to a top-level accessor + * EXCEPT names that collide with a contract envelope field or the reserved + * `namespace` key, which are reachable only via `view.namespace.`. + * + * So `view.public.table.users` works at the root, and `view.storage` always + * remains the contract's `storage` field (never a schema named `storage`). + * Mirrors the facade's `sql.` keying (the default schema keeps its literal + * `__unbound__` id). */ -export type SqlSchemaQualifiedViewShape> = { - readonly [Ns in keyof TContract['storage']['namespaces']]: TContract['storage']['namespaces'][Ns] extends { - readonly entries: infer E; - } - ? SingleNamespaceView - : never; -}; +export type SqlSchemaQualifiedView> = TContract & { + readonly namespace: NamespaceAccessor, SqlBuiltinKind>; +} & PromotedNamespaces, SqlBuiltinKind>; /** - * Builds the schema-qualified SQL view: one single-namespace projection per - * storage namespace, keyed by the raw namespace id (no renaming of the default - * schema). Postgres uses this; SQLite (single default namespace) uses + * Builds the schema-qualified SQL view: the `namespace` accessor holds every + * schema by raw id; non-colliding schema names are promoted to the root. + * Postgres uses this; SQLite (single default namespace) uses * {@link buildSqlSingleNamespaceView}. */ export function buildSqlSchemaQualifiedView>( contract: TContract, -): SqlSchemaQualifiedViewShape { - const out: Record = {}; - for (const [nsId, ns] of Object.entries(contract.storage.namespaces)) { - out[nsId] = promoteBuiltinKinds( - blindCast< - Readonly>, - 'Namespace.entries is the open ADR 224 dictionary Record>' - >(ns.entries), - SQL_BUILTIN_KINDS, - ); - } - return blindCast< - SqlSchemaQualifiedViewShape, - 'each namespace projected to its SingleNamespaceView; keys mirror the storage namespace ids' - >(out); +): SqlSchemaQualifiedView { + const namespaceAccessor = buildNamespaceAccessor< + NamespaceAccessor, SqlBuiltinKind> + >(contract.storage, SQL_BUILTIN_KINDS); + return composeContractView>(contract, {}, namespaceAccessor); } diff --git a/packages/2-sql/1-core/contract/src/exports/contract-view.ts b/packages/2-sql/1-core/contract/src/exports/contract-view.ts index af47db90b0..e418b3fd85 100644 --- a/packages/2-sql/1-core/contract/src/exports/contract-view.ts +++ b/packages/2-sql/1-core/contract/src/exports/contract-view.ts @@ -1,7 +1,8 @@ export type { SqlBuiltinKind, - SqlSchemaQualifiedViewShape, - SqlSingleNamespaceViewShape, + SqlSchemaQualifiedView, + SqlSingleNamespaceAccessors, + SqlSingleNamespaceView, } from '../contract-view'; export { buildSqlSchemaQualifiedView, diff --git a/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts index cde2a6d62d..9ca8854a87 100644 --- a/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts +++ b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts @@ -1,38 +1,45 @@ import type { Contract } from '@prisma-next/contract/types'; import { buildSqlSchemaQualifiedView, - type SqlSchemaQualifiedViewShape, + type SqlSchemaQualifiedView, } from '@prisma-next/sql-contract/contract-view'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; - -export type PostgresContractViewShape> = - SqlSchemaQualifiedViewShape; +import { PostgresContractSerializer } from './postgres-contract-serializer'; /** - * A read-only, schema-qualified view over a deserialized Postgres contract. - * Postgres has named schemas (`public`, `auth`, …) plus the default - * `__unbound__` schema, so the view qualifies by schema first, then applies the - * SQL kind split per schema: + * A schema-qualified Postgres contract view: the deserialized contract + * intersected with per-schema accessors. It is substitutable for `Contract` + * (carries `storage`, `domain`, …) and also exposes each schema's entities: * * ```ts - * const cv = PostgresContractView.from(endContract); - * cv.public.table.users // typed table leaf in the public schema - * cv.auth.table.users // the auth schema's own users table - * cv.public.entries.policy.X // pack-contributed kind (RLS / #771 path) - * cv.__unbound__.table.X // default schema, keyed by its raw id + * const view = PostgresContractView.fromJson(contractJson); + * view.public.table.users // typed table leaf in the public schema + * view.auth.table.users // the auth schema's own users table + * view.public.entries.policy.X // pack-contributed kind (RLS / #771 path) + * view.__unbound__.table.X // default schema, keyed by its raw id + * view.storage // the full contract is still present * ``` * * The schema keys mirror the facade's `sql.` keying exactly — the default * schema is reachable under its literal `__unbound__` id, with no renaming. - * Each schema is its own key; there is no flat cross-namespace merge. The - * `Contract` type is unchanged — this view is a separate object layered on top. + * Each schema is its own key; there is no flat cross-namespace merge. */ -export class PostgresContractView { - private constructor() {} +export type PostgresContractView = Contract> = + SqlSchemaQualifiedView; - static from>( +export const PostgresContractView = { + /** Wrap an already-deserialized Postgres contract in a schema-qualified view. */ + from>( contract: TContract, - ): PostgresContractViewShape { + ): PostgresContractView { + return buildSqlSchemaQualifiedView(contract); + }, + + /** Deserialize a Postgres contract JSON envelope and wrap it in a view. */ + fromJson = Contract>( + json: unknown, + ): PostgresContractView { + const contract = new PostgresContractSerializer().deserializeContract(json); return buildSqlSchemaQualifiedView(contract); - } -} + }, +}; diff --git a/packages/3-targets/3-targets/postgres/src/exports/runtime.ts b/packages/3-targets/3-targets/postgres/src/exports/runtime.ts index 21e44b3e4e..994aa8f5c3 100644 --- a/packages/3-targets/3-targets/postgres/src/exports/runtime.ts +++ b/packages/3-targets/3-targets/postgres/src/exports/runtime.ts @@ -6,7 +6,6 @@ import type { import { postgresTargetDescriptorMetaRuntime } from '../core/descriptor-meta-runtime'; export { PostgresContractSerializer } from '../core/postgres-contract-serializer'; -export type { PostgresContractViewShape } from '../core/postgres-contract-view'; export { PostgresContractView } from '../core/postgres-contract-view'; export interface PostgresRuntimeTargetInstance extends RuntimeTargetInstance<'sql', 'postgres'> {} diff --git a/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts b/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts new file mode 100644 index 0000000000..ff1be01817 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts @@ -0,0 +1,116 @@ +import type { + Contract, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; + +/** + * A hand-built, fully-typed Postgres contract whose namespaces include one named + * `storage` — colliding with the contract envelope's `storage` field — alongside + * a normal `public` schema. There is no committed emitted fixture with a + * `@@schema("storage")` model, so this typed `Contract` stands in to prove the + * collision-safety rule at the type level (and is reused for the runtime test). + * It is a real `Contract` — the view's type behaviour over it is + * identical to behaviour over an emitted contract of the same shape. + */ + +type IdColumn = { + readonly nativeType: 'int4'; + readonly codecId: 'pg/int4@1'; + readonly nullable: false; +}; + +type StorageSchemaTable = { + readonly secrets: { + readonly columns: { readonly id: IdColumn }; + readonly primaryKey: { readonly columns: readonly ['id'] }; + readonly uniques: readonly []; + readonly indexes: readonly []; + readonly foreignKeys: readonly []; + }; +}; + +type PublicSchemaTable = { + readonly widgets: { + readonly columns: { readonly id: IdColumn }; + readonly primaryKey: { readonly columns: readonly ['id'] }; + readonly uniques: readonly []; + readonly indexes: readonly []; + readonly foreignKeys: readonly []; + }; +}; + +export type CollisionContract = Contract< + SqlStorage & { + readonly storageHash: StorageHashBase<'sha256:collision'>; + readonly namespaces: { + readonly storage: { + readonly id: 'storage' & NamespaceId; + readonly kind: 'postgres-schema'; + readonly entries: { readonly table: StorageSchemaTable }; + }; + readonly public: { + readonly id: 'public' & NamespaceId; + readonly kind: 'postgres-schema'; + readonly entries: { readonly table: PublicSchemaTable }; + }; + }; + } +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly profileHash: ProfileHashBase<'sha256:collision'>; +}; + +/** + * A runtime value structurally matching {@link CollisionContract}. Hand-built + * (no serializer round-trip needed) — the view layering is structural and does + * not depend on hydrated IR-class identity for this collision check. + */ +export const collisionContractValue = { + target: 'postgres', + targetFamily: 'sql', + profileHash: 'sha256:collision', + capabilities: {}, + extensionPacks: {}, + meta: {}, + roots: {}, + domain: { namespaces: {} }, + storage: { + storageHash: 'sha256:collision', + namespaces: { + storage: { + id: 'storage', + kind: 'postgres-schema', + entries: { + table: { + secrets: { + columns: { id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false } }, + primaryKey: { columns: ['id'] }, + uniques: [], + indexes: [], + foreignKeys: [], + }, + }, + }, + }, + public: { + id: 'public', + kind: 'postgres-schema', + entries: { + table: { + widgets: { + columns: { id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false } }, + primaryKey: { columns: ['id'] }, + uniques: [], + indexes: [], + foreignKeys: [], + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts index 7975e9e0f6..330f0eece6 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts @@ -1,17 +1,30 @@ +import type { Contract as ContractBase } from '@prisma-next/contract/types'; import { expectTypeOf, test } from 'vitest'; -import type { PostgresContractView } from '../src/core/postgres-contract-view'; +import { PostgresContractView } from '../src/core/postgres-contract-view'; +import type { CollisionContract } from './fixtures/collision-contract'; import type { Contract } from './fixtures/namespaced-contract.d'; /** * Emit-then-consume type tests against a REAL emitted multi-schema Postgres * contract (`test/fixtures/namespaced-contract.d.ts`). The fixture declares the * SAME bare table `users` in BOTH `public` (column `email`) and `auth` (column - * `token`) — the discriminator that proves per-schema qualification. + * `token`) — the discriminator that proves per-schema qualification. The + * collision tests additionally use a typed hand-built contract whose schema is + * named `storage` (see `fixtures/collision-contract.ts`). */ -type CV = ReturnType>; +type CV = PostgresContractView; -test('each schema is its own key with its own table columns', () => { +test('the view is assignable to Contract (superset)', () => { + expectTypeOf().toMatchTypeOf(); +}); + +test('from() and fromJson() both return the view type', () => { + expectTypeOf(PostgresContractView.from).returns.toEqualTypeOf(); + expectTypeOf(PostgresContractView.fromJson).returns.toEqualTypeOf(); +}); + +test('each schema is its own key with its own table columns (root promotion)', () => { expectTypeOf< CV['public']['table']['users']['columns']['email']['codecId'] >().toEqualTypeOf<'pg/text@1'>(); @@ -20,24 +33,33 @@ test('each schema is its own key with its own table columns', () => { >().toEqualTypeOf<'pg/text@1'>(); }); +test('view.namespace. reaches every schema by raw id', () => { + expectTypeOf< + CV['namespace']['public']['table']['users']['columns']['email']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); + expectTypeOf< + CV['namespace']['auth']['table']['users']['columns']['token']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); +}); + test('cross-schema column access is a compile error', () => { - const cv = null as unknown as CV; + const view = null as unknown as CV; // @ts-expect-error public.users has no `token` column (that is auth.users) - cv.public.table.users.columns.token; + view.public.table.users.columns.token; // @ts-expect-error auth.users has no `email` column (that is public.users) - cv.auth.table.users.columns.email; + view.auth.table.users.columns.email; }); test('a non-existent schema key is a compile error', () => { - const cv = null as unknown as CV; + const view = null as unknown as CV; // @ts-expect-error 'marketing' is not an emitted schema - cv.marketing; + view.marketing; }); test('a non-existent table name in a schema is a compile error', () => { - const cv = null as unknown as CV; + const view = null as unknown as CV; // @ts-expect-error 'orders' is not a table in the public schema - cv.public.table.orders; + view.public.table.orders; }); test('the cross-schema foreign key on public.profile is reachable', () => { @@ -51,10 +73,41 @@ test('valueSet slot is present per schema (none emitted, so empty maps)', () => expectTypeOf().toEqualTypeOf>(); }); -test('cv..entries excludes the built-in table and valueSet keys', () => { +test('view..entries excludes the built-in table and valueSet keys', () => { type PublicEntries = CV['public']['entries']; type HasTable = 'table' extends keyof PublicEntries ? true : false; type HasValueSet = 'valueSet' extends keyof PublicEntries ? true : false; expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); + +// --- Collision: a schema named `storage` must not shadow the contract field --- + +type Collision = PostgresContractView; + +test('view.storage stays the contract field even when a schema is named `storage`', () => { + // The contract envelope field wins at the root: `view.storage` carries + // `.namespaces`, NOT the schema view (which would have `.table`). + expectTypeOf().toMatchTypeOf(); + type StorageHasTable = 'table' extends keyof Collision['storage'] ? true : false; + expectTypeOf().toEqualTypeOf(); +}); + +test('the `storage`-named schema is reachable via view.namespace.storage', () => { + expectTypeOf< + Collision['namespace']['storage']['table']['secrets']['columns']['id']['codecId'] + >().toEqualTypeOf<'pg/int4@1'>(); +}); + +test('a non-colliding schema (`public`) is still promoted to the root', () => { + expectTypeOf< + Collision['public']['table']['widgets']['columns']['id']['codecId'] + >().toEqualTypeOf<'pg/int4@1'>(); + expectTypeOf< + Collision['namespace']['public']['table']['widgets']['columns']['id']['codecId'] + >().toEqualTypeOf<'pg/int4@1'>(); +}); + +test('the collision view is still assignable to Contract', () => { + expectTypeOf().toMatchTypeOf(); +}); diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts index 28cb28b996..3240508d5f 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { PostgresContractSerializer } from '../src/core/postgres-contract-serializer'; import { PostgresContractView } from '../src/core/postgres-contract-view'; +import { type CollisionContract, collisionContractValue } from './fixtures/collision-contract'; import type { Contract } from './fixtures/namespaced-contract.d'; import contractJson from './fixtures/namespaced-contract.json' with { type: 'json' }; @@ -11,30 +12,52 @@ describe('PostgresContractView', () => { expect(PostgresContractView.from(contract)).toBeDefined(); }); + it('the view is a superset of the contract (contract fields present)', () => { + const view = PostgresContractView.from(contract); + expect(view.storage).toBe(contract.storage); + expect(view.domain).toBe(contract.domain); + expect(view.roots).toBe(contract.roots); + }); + it('keys each schema separately with its own tables', () => { - const cv = PostgresContractView.from(contract); - expect(cv.public.table.users).toBeDefined(); - expect(cv.auth.table.users).toBeDefined(); - expect(Object.keys(cv.public.table.users.columns).sort()).toEqual(['email', 'id']); - expect(Object.keys(cv.auth.table.users.columns).sort()).toEqual(['id', 'token']); + const view = PostgresContractView.from(contract); + expect(view.public.table.users).toBeDefined(); + expect(view.auth.table.users).toBeDefined(); + expect(Object.keys(view.public.table.users.columns).sort()).toEqual(['email', 'id']); + expect(Object.keys(view.auth.table.users.columns).sort()).toEqual(['id', 'token']); + }); + + it('view..table. returns the same entity object as the raw contract', () => { + const view = PostgresContractView.from(contract); + expect(view.public.table.users).toBe( + contract.storage.namespaces['public']?.entries.table?.users, + ); + expect(view.auth.table.users).toBe(contract.storage.namespaces['auth']?.entries.table?.users); }); - it('cv..table. returns the same entity object as the raw contract', () => { - const cv = PostgresContractView.from(contract); - expect(cv.public.table.users).toBe(contract.storage.namespaces['public']?.entries.table?.users); - expect(cv.auth.table.users).toBe(contract.storage.namespaces['auth']?.entries.table?.users); + it('view.namespace. reaches every schema by raw id', () => { + const view = PostgresContractView.from(contract); + expect(view.namespace.public.table.users).toBe(view.public.table.users); + expect(view.namespace.auth.table.users).toBe(view.auth.table.users); }); - it('cv..valueSet is present and empty (no value sets emitted)', () => { - const cv = PostgresContractView.from(contract); - expect(cv.public.valueSet).toEqual({}); - expect(cv.auth.valueSet).toEqual({}); + it('view..valueSet is present and empty (no value sets emitted)', () => { + const view = PostgresContractView.from(contract); + expect(view.public.valueSet).toEqual({}); + expect(view.auth.valueSet).toEqual({}); }); - it('cv..entries excludes the built-in table and valueSet keys', () => { - const cv = PostgresContractView.from(contract); - expect(Object.keys(cv.public.entries)).not.toContain('table'); - expect(Object.keys(cv.public.entries)).not.toContain('valueSet'); + it('view..entries excludes the built-in table and valueSet keys', () => { + const view = PostgresContractView.from(contract); + expect(Object.keys(view.public.entries)).not.toContain('table'); + expect(Object.keys(view.public.entries)).not.toContain('valueSet'); + }); + + it('fromJson() deserializes and wraps in one call', () => { + const view = PostgresContractView.fromJson(contractJson); + expect(view.public.table.users).toBeDefined(); + expect(view.auth.table.users).toBeDefined(); + expect(view.storage.storageHash).toBe(contract.storage.storageHash); }); it('the default __unbound__ schema is keyed by its raw id (mirrors the facade)', () => { @@ -56,14 +79,14 @@ describe('PostgresContractView', () => { }, } as unknown as Contract; - const cv = PostgresContractView.from(withDefault) as Record< + const view = PostgresContractView.from(withDefault) as unknown as Record< string, { table: Record } >; - expect(cv['__unbound__']?.table['widgets']).toBeDefined(); + expect(view['__unbound__']?.table['widgets']).toBeDefined(); }); - it('cv..entries exposes pack-contributed kinds', () => { + it('view..entries exposes pack-contributed kinds', () => { // RLS `policy` isn't in a committed fixture yet, so hand-build a contract // with a pack-contributed `policy` kind under the public schema. const fakePolicy = { name: 'read_all' }; @@ -84,9 +107,36 @@ describe('PostgresContractView', () => { }, } as unknown as Contract; - const cv = PostgresContractView.from(withPolicy); - expect((cv.public.entries as Record)['policy']).toEqual({ + const view = PostgresContractView.from(withPolicy); + expect((view.public.entries as Record)['policy']).toEqual({ readAll: fakePolicy, }); }); + + describe('schema-name collision with a contract field', () => { + const collision = collisionContractValue as unknown as CollisionContract; + + it('view.storage stays the contract field (not the schema named `storage`)', () => { + const view = PostgresContractView.from(collision); + expect(view.storage).toBe(collision.storage); + expect(view.storage.namespaces).toBeDefined(); + // The contract field has no `.table` — the schema view would. + expect((view.storage as unknown as Record)['table']).toBeUndefined(); + }); + + it('the `storage`-named schema is reachable via view.namespace.storage', () => { + const view = PostgresContractView.from(collision); + expect(view.namespace.storage.table.secrets).toBe( + collision.storage.namespaces.storage.entries.table.secrets, + ); + }); + + it('a non-colliding schema (`public`) is promoted to the root AND under namespace', () => { + const view = PostgresContractView.from(collision); + expect(view.public.table.widgets).toBe( + collision.storage.namespaces.public.entries.table.widgets, + ); + expect(view.namespace.public.table.widgets).toBe(view.public.table.widgets); + }); + }); }); diff --git a/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts b/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts index 742f762e3f..47e16cad43 100644 --- a/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts +++ b/packages/3-targets/3-targets/sqlite/src/core/sqlite-contract-view.ts @@ -1,35 +1,41 @@ import type { Contract } from '@prisma-next/contract/types'; import { buildSqlSingleNamespaceView, - type SqlSingleNamespaceViewShape, + type SqlSingleNamespaceView, } from '@prisma-next/sql-contract/contract-view'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; - -export type SqliteContractViewShape> = - SqlSingleNamespaceViewShape; +import { SqliteContractSerializer } from './sqlite-contract-serializer'; /** - * A read-only view over a deserialized SQLite contract that unwraps the single - * default namespace and promotes the SQL built-in kinds to the top level. + * A SQLite contract view: the deserialized contract intersected with by-name + * accessors. It is substitutable for `Contract` (carries `storage`, `domain`, + * …) and also exposes the single default namespace unwrapped with the SQL + * built-in kinds promoted: * - * Usage: * ```ts - * const cv = SqliteContractView.from(endContract); - * cv.table.users // typed table leaf - * cv.entries.policy.X // pack-contributed kind (singular key) + * const view = SqliteContractView.fromJson(contractJson); + * view.table.users // typed table leaf + * view.entries.policy.X // pack-contributed kind (singular key) + * view.storage // the full contract is still present * ``` * * SQLite has `sql.enums: false`, so it never emits `valueSet` entries; the - * `valueSet` slot is therefore an empty map. The `Contract` type is unchanged — - * this view is a separate object layered on top, reusing the generic - * single-namespace projection. + * `valueSet` slot is therefore an empty map. */ -export class SqliteContractView { - private constructor() {} +export type SqliteContractView = Contract> = + SqlSingleNamespaceView; + +export const SqliteContractView = { + /** Wrap an already-deserialized SQLite contract in a view. */ + from>(contract: TContract): SqliteContractView { + return buildSqlSingleNamespaceView(contract); + }, - static from>( - contract: TContract, - ): SqliteContractViewShape { + /** Deserialize a SQLite contract JSON envelope and wrap it in a view. */ + fromJson = Contract>( + json: unknown, + ): SqliteContractView { + const contract = new SqliteContractSerializer().deserializeContract(json); return buildSqlSingleNamespaceView(contract); - } -} + }, +}; diff --git a/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts b/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts index db7eec59c2..32921c17ed 100644 --- a/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts +++ b/packages/3-targets/3-targets/sqlite/src/exports/runtime.ts @@ -1,5 +1,4 @@ export type { SqliteRuntimeTargetInstance } from '../core/runtime-target'; export { default } from '../core/runtime-target'; export { SqliteContractSerializer } from '../core/sqlite-contract-serializer'; -export type { SqliteContractViewShape } from '../core/sqlite-contract-view'; export { SqliteContractView } from '../core/sqlite-contract-view'; diff --git a/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json index eba0d49db1..0f3383a1cf 100644 --- a/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json +++ b/packages/3-targets/3-targets/sqlite/test/fixtures/sqlite-contract.json @@ -557,8 +557,10 @@ }, "capabilities": { "sql": { + "enums": false, "foreignKeys": true, "jsonAgg": true, + "lateral": false, "limit": true, "orderBy": true, "returning": true diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts index 0a193a4e57..cf690a0f67 100644 --- a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test-d.ts @@ -1,5 +1,6 @@ +import type { Contract as ContractBase } from '@prisma-next/contract/types'; import { expectTypeOf, test } from 'vitest'; -import type { SqliteContractView } from '../src/core/sqlite-contract-view'; +import { SqliteContractView } from '../src/core/sqlite-contract-view'; import type { Contract } from './fixtures/sqlite-contract.d'; /** @@ -8,9 +9,18 @@ import type { Contract } from './fixtures/sqlite-contract.d'; * view type against the actual emitted shape, not a hand-authored `typeof`. */ -type CV = ReturnType>; +type CV = SqliteContractView; -test('cv.table. resolves to the concrete emitted table leaf', () => { +test('the view is assignable to Contract (superset)', () => { + expectTypeOf().toMatchTypeOf(); +}); + +test('from() and fromJson() both return the view type', () => { + expectTypeOf(SqliteContractView.from).returns.toEqualTypeOf(); + expectTypeOf(SqliteContractView.fromJson).returns.toEqualTypeOf(); +}); + +test('view.table. resolves to the concrete emitted table leaf', () => { expectTypeOf< CV['table']['users']['columns']['id']['codecId'] >().toEqualTypeOf<'sqlite/integer@1'>(); @@ -29,17 +39,23 @@ test('multiple tables are reachable top-level', () => { >().toEqualTypeOf<'sqlite/text@1'>(); }); +test('view.namespace.__unbound__ reaches the default namespace by id', () => { + expectTypeOf< + CV['namespace']['__unbound__']['table']['users']['columns']['id']['codecId'] + >().toEqualTypeOf<'sqlite/integer@1'>(); +}); + test('a non-existent table name is a compile error', () => { - const cv = null as unknown as CV; + const view = null as unknown as CV; // @ts-expect-error 'nonexistent' is not an emitted table - cv.table.nonexistent; + view.table.nonexistent; }); test('valueSet slot is present (SQLite emits none, so it is an empty map)', () => { expectTypeOf().toEqualTypeOf>(); }); -test('cv.entries does not contain the built-in table or valueSet keys', () => { +test('view.entries does not contain the built-in table or valueSet keys', () => { type Entries = CV['entries']; type HasTable = 'table' extends keyof Entries ? true : false; type HasValueSet = 'valueSet' extends keyof Entries ? true : false; diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts index edcb00f8a1..79305aa566 100644 --- a/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-contract-view.test.ts @@ -12,30 +12,49 @@ describe('SqliteContractView', () => { expect(SqliteContractView.from(contract)).toBeDefined(); }); - it('cv.table exposes tables from the default namespace', () => { - const cv = SqliteContractView.from(contract); - expect(cv.table.users).toBeDefined(); - expect(cv.table.posts).toBeDefined(); + it('the view is a superset of the contract (contract fields present)', () => { + const view = SqliteContractView.from(contract); + expect(view.storage).toBe(contract.storage); + expect(view.domain).toBe(contract.domain); + expect(view.roots).toBe(contract.roots); }); - it('cv.table. returns the same entity object as the raw contract', () => { - const cv = SqliteContractView.from(contract); + it('view.table exposes tables from the default namespace', () => { + const view = SqliteContractView.from(contract); + expect(view.table.users).toBeDefined(); + expect(view.table.posts).toBeDefined(); + }); + + it('view.table. returns the same entity object as the raw contract', () => { + const view = SqliteContractView.from(contract); const rawTables = contract.storage.namespaces[UNBOUND_NAMESPACE_ID].entries.table; - expect(cv.table.users).toBe(rawTables?.users); + expect(view.table.users).toBe(rawTables?.users); + }); + + it('view.namespace.__unbound__ reaches the default namespace by id', () => { + const view = SqliteContractView.from(contract); + expect(view.namespace[UNBOUND_NAMESPACE_ID].table.users).toBe(view.table.users); + }); + + it('view.valueSet is present and empty (SQLite emits no value sets)', () => { + const view = SqliteContractView.from(contract); + expect(view.valueSet).toEqual({}); }); - it('cv.valueSet is present and empty (SQLite emits no value sets)', () => { - const cv = SqliteContractView.from(contract); - expect(cv.valueSet).toEqual({}); + it('view.entries does not contain the built-in table or valueSet keys', () => { + const view = SqliteContractView.from(contract); + expect(Object.keys(view.entries)).not.toContain('table'); + expect(Object.keys(view.entries)).not.toContain('valueSet'); }); - it('cv.entries does not contain the built-in table or valueSet keys', () => { - const cv = SqliteContractView.from(contract); - expect(Object.keys(cv.entries)).not.toContain('table'); - expect(Object.keys(cv.entries)).not.toContain('valueSet'); + it('fromJson() deserializes and wraps in one call', () => { + const view = SqliteContractView.fromJson(contractJson); + expect(view.table.users).toBeDefined(); + // Substitutable for Contract: storage hash matches the serializer's output. + expect(view.storage.storageHash).toBe(contract.storage.storageHash); }); - it('cv.entries exposes pack-contributed kinds', () => { + it('view.entries exposes pack-contributed kinds', () => { // SQLite emits only the built-in `table` kind, so this hand-builds a // contract with an extra pack-contributed `policy` kind to prove non-built-in // kinds land under `.entries`. @@ -56,7 +75,7 @@ describe('SqliteContractView', () => { }, } as unknown as Contract; - const cv = SqliteContractView.from(contractWithPackKind); - expect((cv.entries as Record)['policy']).toEqual({ readPolicy: fakeEntry }); + const view = SqliteContractView.from(contractWithPackKind); + expect((view.entries as Record)['policy']).toEqual({ readPolicy: fakeEntry }); }); }); From 8233b076e3aab539b95c88c4463bd302b079fdc2 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 29 Jun 2026 12:29:57 +0200 Subject: [PATCH 04/12] refactor(tml-2892): align ContractView with the NamespacedEnums projection (#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.`. Co-Authored-By: Claude Opus 4.8 Signed-off-by: willbot Signed-off-by: Will Madden --- .../framework-components/src/exports/ir.ts | 6 +- .../src/ir/contract-view.ts | 100 ++++----------- .../test/contract-view.test.ts | 80 +++--------- .../mongo-contract/src/contract-view.ts | 39 +++--- .../1-core/contract/src/contract-view.ts | 80 ++++++------ .../src/core/postgres-contract-view.ts | 23 ++-- .../test/fixtures/collision-contract.ts | 116 ------------------ .../test/postgres-contract-view.test-d.ts | 73 +++-------- .../test/postgres-contract-view.test.ts | 89 +++++--------- 9 files changed, 163 insertions(+), 443 deletions(-) delete mode 100644 packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts diff --git a/packages/1-framework/1-core/framework-components/src/exports/ir.ts b/packages/1-framework/1-core/framework-components/src/exports/ir.ts index 1db489449b..c26d30a29b 100644 --- a/packages/1-framework/1-core/framework-components/src/exports/ir.ts +++ b/packages/1-framework/1-core/framework-components/src/exports/ir.ts @@ -1,13 +1,11 @@ export type { DefaultNamespaceEntries, - NamespaceAccessor, - PromotedNamespaces, + NamespacedEntities, SingleNamespaceView, } from '../ir/contract-view'; export { - buildNamespaceAccessor, + buildNamespacedEntities, buildSingleNamespaceView, - composeContractView, promoteBuiltinKinds, } from '../ir/contract-view'; export { domainElementCoordinates } from '../ir/domain'; diff --git a/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts index 8950d3bf01..1b3c92f62b 100644 --- a/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts +++ b/packages/1-framework/1-core/framework-components/src/ir/contract-view.ts @@ -13,8 +13,9 @@ export type DefaultNamespaceEntries = { type EntriesOf = TNamespace extends { readonly entries: infer E } ? E : never; /** - * The `namespace` accessor: every storage namespace keyed by its raw id, each - * projected to its {@link SingleNamespaceView}. Fully qualified and - * collision-proof — `view.namespace.` reaches any namespace by id even when - * the name collides with a contract envelope field. + * The namespace-keyed entity-view map — every storage namespace keyed by its raw + * id, each projected to its {@link SingleNamespaceView}. Mirrors + * `NamespacedEnums` from `@prisma-next/contract/enum-accessor`: the migration + * author's storage-side `view.namespace.` is the twin of the runtime's + * `db.enums.`. Nesting the schema map under one fixed `namespace` member + * makes it collision-proof — a schema named `storage` is `view.namespace.storage`, + * never a contract-root key. */ -export type NamespaceAccessor = { - readonly [Ns in keyof TNamespaces]: SingleNamespaceView< - EntriesOf, +export type NamespacedEntities< + TStorage extends { readonly namespaces: object }, + TBuiltinKinds extends string, +> = { + readonly [Ns in keyof TStorage['namespaces']]: SingleNamespaceView< + EntriesOf, TBuiltinKinds >; }; -/** - * Root-promoted namespaces: each namespace name promoted to a top-level - * accessor, EXCEPT names that collide with a contract envelope field (`keyof - * TContract`) or with the reserved `namespace` key. Those excluded names stay - * reachable only via {@link NamespaceAccessor}. The key-remapping `as ... ? - * never : Ns` is what keeps the root promotion from shadowing a contract field - * at the type level. - */ -export type PromotedNamespaces = { - readonly [Ns in keyof TNamespaces as Ns extends keyof TContract | 'namespace' - ? never - : Ns]: SingleNamespaceView, TBuiltinKinds>; -}; - /** * Projects one namespace's `entries` into the view shape: each built-in kind * becomes a top-level slot (materialized empty if absent), and the remaining * pack-contributed kinds sit under `.entries`. Shared by the single-namespace - * builder and the per-schema (multi-namespace) Postgres builder. + * builder and the namespace-map builder. */ export function promoteBuiltinKinds( entries: Readonly>, @@ -99,10 +92,9 @@ export function promoteBuiltinKinds( } /** - * Builds the runtime accessor object for a single-namespace contract: unwraps - * the default namespace and promotes the given built-in kind slots to top-level. - * The static type is supplied by the caller via the generic factory; this - * function is structural. + * Builds one namespace's entity view: promotes the given built-in kind slots to + * top-level for the default (`UNBOUND_NAMESPACE_ID`) namespace. Single-namespace + * targets (Mongo, SQLite) use this to unwrap their sole namespace to the root. * * Throws if the contract has no default (`UNBOUND_NAMESPACE_ID`) namespace. */ @@ -122,13 +114,14 @@ export function buildSingleNamespaceView( } /** - * Builds the per-namespace accessor map (`{ : SingleNamespaceView }`) for - * every namespace in the storage, keyed by raw namespace id. + * Builds the namespace-keyed entity-view map (`{ : SingleNamespaceView }`) + * for every namespace in the storage, keyed by raw namespace id. Mirrors + * `buildNamespacedEnums(domain)` — the storage-side twin. */ -export function buildNamespaceAccessor( +export function buildNamespacedEntities( storage: Storage, builtinKinds: readonly string[], -): TAccessor { +): TMap { const out: Record = {}; for (const [nsId, ns] of Object.entries(storage.namespaces)) { out[nsId] = promoteBuiltinKinds( @@ -140,48 +133,7 @@ export function buildNamespaceAccessor( ); } return blindCast< - TAccessor, + TMap, 'each namespace projected to its SingleNamespaceView; keys mirror the storage namespace ids' >(out); } - -/** - * Composes a contract view from the deserialized contract plus its accessors, - * enforcing collision-safe layering so the result stays substitutable for the - * contract: - * - * 1. **Contract envelope fields win at the root.** Every contract own field - * (`storage`, `domain`, `roots`, …) is copied first and is never overwritten. - * 2. **`rootAccessors`** (kind-promoted slots for the single-namespace families, - * e.g. `collection`/`table`/`entries`) are layered next; for Postgres this is - * empty. - * 3. **`namespace`** holds every namespace by id — the fully-qualified, - * collision-proof accessor. - * 4. **Root-promoted namespace names** are added last, but ONLY for names not - * already present at the root (not a contract field, not `namespace`, not an - * already-promoted kind slot). A namespace named `storage` is therefore - * reachable only via `view.namespace.storage`, while `view.storage` stays the - * contract's storage. - */ -export function composeContractView( - contract: object, - rootAccessors: Readonly>, - namespaceAccessor: Readonly>, -): TView { - const root: Record = { ...contract }; - for (const [key, value] of Object.entries(rootAccessors)) { - if (!(key in root)) { - root[key] = value; - } - } - root['namespace'] = namespaceAccessor; - for (const [nsId, nsView] of Object.entries(namespaceAccessor)) { - if (!(nsId in root)) { - root[nsId] = nsView; - } - } - return blindCast< - TView, - 'root carries every contract field, the kind-promoted root accessors, the namespace map, and the non-colliding promoted namespace names; the view type is the caller-supplied intersection' - >(root); -} diff --git a/packages/1-framework/1-core/framework-components/test/contract-view.test.ts b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts index 59f46de819..7d6e8d289a 100644 --- a/packages/1-framework/1-core/framework-components/test/contract-view.test.ts +++ b/packages/1-framework/1-core/framework-components/test/contract-view.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { - buildNamespaceAccessor, - buildSingleNamespaceView, - composeContractView, -} from '../src/ir/contract-view'; +import { buildNamespacedEntities, buildSingleNamespaceView } from '../src/ir/contract-view'; import { UNBOUND_NAMESPACE_ID } from '../src/ir/namespace'; import type { Storage } from '../src/ir/storage'; @@ -67,13 +63,13 @@ describe('buildSingleNamespaceView', () => { }); }); -describe('buildNamespaceAccessor', () => { - it('keys every namespace by raw id, each kind-promoted', () => { +describe('buildNamespacedEntities', () => { + it('keys every namespace by raw id, each kind-promoted (mirrors buildNamespacedEnums)', () => { const storage = multiNamespaceStorage({ public: { table: { users: { c: 1 } } }, auth: { table: { sessions: { c: 2 } } }, }); - const ns = buildNamespaceAccessor<{ + const ns = buildNamespacedEntities<{ public: { table: { users: unknown }; entries: object }; auth: { table: { sessions: unknown }; entries: object }; }>(storage, ['table', 'valueSet']); @@ -82,64 +78,18 @@ describe('buildNamespaceAccessor', () => { expect(ns.public.table.users).toEqual({ c: 1 }); expect(ns.auth.table.sessions).toEqual({ c: 2 }); }); -}); - -describe('composeContractView', () => { - it('contract fields win at the root; root accessors and namespace are added', () => { - const contract = { storage: { ns: 1 }, domain: { models: {} }, roots: {} }; - const rootAccessors = { table: { users: {} }, entries: {} }; - const namespaceAccessor = { __unbound__: { table: { users: {} }, entries: {} } }; - const view = composeContractView< - typeof contract & typeof rootAccessors & { namespace: typeof namespaceAccessor } - >(contract, rootAccessors, namespaceAccessor); - - expect(view.storage).toBe(contract.storage); - expect(view.table).toBe(rootAccessors.table); - expect(view.namespace).toBe(namespaceAccessor); - expect(view).not.toBe(contract); - }); - - it('promotes non-colliding namespace names to the root', () => { - const contract = { storage: {}, domain: {}, roots: {} }; - const namespaceAccessor = { - public: { table: { widgets: {} } }, - auth: { table: { sessions: {} } }, - }; - const view = composeContractView>(contract, {}, namespaceAccessor); - - expect(view['public']).toBe(namespaceAccessor.public); - expect(view['auth']).toBe(namespaceAccessor.auth); - }); - it('does NOT promote a namespace whose name collides with a contract field', () => { - const contract = { storage: { isContractField: true }, domain: {}, roots: {} }; - const namespaceAccessor = { - storage: { table: { secrets: {} } }, - public: { table: { widgets: {} } }, - }; - const view = composeContractView>(contract, {}, namespaceAccessor); - - // The contract field wins at the root. - expect(view['storage']).toBe(contract.storage); - expect((view['storage'] as Record)['table']).toBeUndefined(); - // The colliding schema is reachable only via the namespace accessor. - expect((view['namespace'] as Record)['storage']).toBe( - namespaceAccessor.storage, - ); - // A non-colliding schema is still promoted. - expect(view['public']).toBe(namespaceAccessor.public); - }); - - it('does NOT let a namespace named `namespace` overwrite the namespace accessor', () => { - const contract = { storage: {}, domain: {}, roots: {} }; - const namespaceAccessor = { namespace: { table: { x: {} } }, public: { table: { y: {} } } }; - const view = composeContractView>(contract, {}, namespaceAccessor); + it('a schema named `storage` is a normal key under the map (collision-proof by nesting)', () => { + const storage = multiNamespaceStorage({ + storage: { table: { secrets: { c: 1 } } }, + public: { table: { widgets: { c: 2 } } }, + }); + const ns = buildNamespacedEntities<{ + storage: { table: { secrets: unknown }; entries: object }; + public: { table: { widgets: unknown }; entries: object }; + }>(storage, ['table', 'valueSet']); - // `view.namespace` is the accessor map (holds both ids), not the schema view. - expect(view['namespace']).toBe(namespaceAccessor); - expect((view['namespace'] as Record)['namespace']).toBe( - namespaceAccessor.namespace, - ); - expect(view['public']).toBe(namespaceAccessor.public); + expect(ns.storage.table.secrets).toEqual({ c: 1 }); + expect(ns.public.table.widgets).toEqual({ c: 2 }); }); }); diff --git a/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts index d75b227c54..90fd438629 100644 --- a/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts +++ b/packages/2-mongo-family/1-foundation/mongo-contract/src/contract-view.ts @@ -1,9 +1,8 @@ import { - buildNamespaceAccessor, + buildNamespacedEntities, buildSingleNamespaceView, - composeContractView, type DefaultNamespaceEntries, - type NamespaceAccessor, + type NamespacedEntities, type SingleNamespaceView, } from '@prisma-next/framework-components/ir'; import type { MongoContract } from './contract-types'; @@ -13,8 +12,6 @@ type MongoBuiltinKind = (typeof MONGO_BUILTIN_KINDS)[number]; type MongoEntries = DefaultNamespaceEntries; -type MongoNamespaces = TContract['storage']['namespaces']; - /** * The Mongo accessors: the built-in `collection` kind promoted to a top-level * accessor, pack-contributed kinds under `entries` (singular keys). @@ -28,10 +25,12 @@ export type MongoContractAccessors = SingleName * A Mongo contract view: the deserialized contract intersected with the by-name * accessors, so the value is substitutable for `Contract` (carries `storage`, * `domain`, …) while also exposing: - * - `view.collection.` — the built-in kind, default namespace unwrapped. + * - `view.collection.` — the built-in kind, sole namespace unwrapped to + * the root (Mongo is single-namespace). * - `view.entries.` — pack-contributed kinds (singular keys). - * - `view.namespace.` — every namespace by raw id (Mongo's sole namespace - * is `__unbound__`), the fully-qualified collision-proof accessor. + * - `view.namespace.` — the namespace-keyed entity map (Mongo's sole + * namespace is `__unbound__`). This mirrors the runtime `db.enums` pattern: + * a single fixed `namespace` member, collision-proof. * * The factory (`MongoContractView.from` / `.fromJson`) lives in * `@prisma-next/family-mongo/ir`, where the Mongo serializer is reachable; this @@ -39,14 +38,14 @@ export type MongoContractAccessors = SingleName */ export type MongoContractView = TContract & MongoContractAccessors & { - readonly namespace: NamespaceAccessor, MongoBuiltinKind>; + readonly namespace: NamespacedEntities; }; /** - * Builds the Mongo view: unwraps the default namespace, promotes the built-in - * `collection` kind at the root, attaches the `namespace` accessor, and layers - * everything over the deserialized contract so the result is a structural - * superset of the contract. + * Builds the Mongo view: unwraps the sole namespace's built-in `collection` kind + * to the root, attaches the namespace-keyed `namespace` map, and layers both + * over the deserialized contract so the result is a structural superset of the + * contract. */ export function buildMongoContractView( contract: TContract, @@ -55,12 +54,12 @@ export function buildMongoContractView( contract.storage, MONGO_BUILTIN_KINDS, ); - const namespaceAccessor = buildNamespaceAccessor< - NamespaceAccessor, MongoBuiltinKind> + const namespace = buildNamespacedEntities< + NamespacedEntities >(contract.storage, MONGO_BUILTIN_KINDS); - return composeContractView>( - contract, - rootAccessors, - namespaceAccessor, - ); + return { + ...contract, + ...rootAccessors, + namespace, + }; } diff --git a/packages/2-sql/1-core/contract/src/contract-view.ts b/packages/2-sql/1-core/contract/src/contract-view.ts index 8a694dcb24..cde4ecba6c 100644 --- a/packages/2-sql/1-core/contract/src/contract-view.ts +++ b/packages/2-sql/1-core/contract/src/contract-view.ts @@ -1,11 +1,9 @@ import type { Contract } from '@prisma-next/contract/types'; import { - buildNamespaceAccessor, + buildNamespacedEntities, buildSingleNamespaceView, - composeContractView, type DefaultNamespaceEntries, - type NamespaceAccessor, - type PromotedNamespaces, + type NamespacedEntities, type SingleNamespaceView, } from '@prisma-next/framework-components/ir'; import type { SqlStorage } from './ir/sql-storage'; @@ -22,8 +20,6 @@ type SqlEntries> = DefaultNamespaceEntrie TContract['storage'] >; -type SqlNamespaces> = TContract['storage']['namespaces']; - /** * The single-namespace SQL accessors: `table`/`valueSet` top-level, pack kinds * under `entries`. A target that never emits a built-in kind (SQLite has @@ -37,21 +33,22 @@ export type SqlSingleNamespaceAccessors> * Single-namespace SQL view: the deserialized contract intersected with the * by-name accessors, so the value is substitutable for `Contract` while also * exposing: - * - `view.table.` / `view.valueSet.` — built-in kinds, default - * namespace unwrapped; pack kinds under `view.entries.`. - * - `view.namespace.` — every namespace by raw id (SQLite's sole namespace - * is `__unbound__`). + * - `view.table.` / `view.valueSet.` — built-in kinds, sole + * namespace unwrapped to the root; pack kinds under `view.entries.`. + * - `view.namespace.` — the namespace-keyed entity map (SQLite's sole + * namespace is `__unbound__`). Mirrors the runtime `db.enums` pattern. */ export type SqlSingleNamespaceView> = TContract & SqlSingleNamespaceAccessors & { - readonly namespace: NamespaceAccessor, SqlBuiltinKind>; + readonly namespace: NamespacedEntities; }; /** - * Builds the single-namespace SQL view: promotes the SQL built-in-kind accessors - * (`table`, `valueSet`) at the root, attaches the `namespace` accessor, and - * layers everything over the deserialized contract. Targets with one default - * namespace (SQLite) call this directly; Postgres qualifies by schema. + * Builds the single-namespace SQL view: unwraps the sole namespace's SQL + * built-in kinds (`table`, `valueSet`) to the root, attaches the namespace-keyed + * `namespace` map, and layers both over the deserialized contract. Targets with + * one default namespace (SQLite) call this directly; Postgres qualifies by + * schema. */ export function buildSqlSingleNamespaceView>( contract: TContract, @@ -60,45 +57,42 @@ export function buildSqlSingleNamespaceView, SqlBuiltinKind> + const namespace = buildNamespacedEntities< + NamespacedEntities >(contract.storage, SQL_BUILTIN_KINDS); - return composeContractView>( - contract, - rootAccessors, - namespaceAccessor, - ); + return { + ...contract, + ...rootAccessors, + namespace, + }; } /** - * Schema-qualified SQL view: the deserialized contract intersected with - * - `view.namespace.` — every schema by raw id (the fully-qualified, - * collision-proof accessor; `view.namespace.storage` is the schema literally - * named `storage`), and - * - root-promoted schema names — each schema promoted to a top-level accessor - * EXCEPT names that collide with a contract envelope field or the reserved - * `namespace` key, which are reachable only via `view.namespace.`. - * - * So `view.public.table.users` works at the root, and `view.storage` always - * remains the contract's `storage` field (never a schema named `storage`). - * Mirrors the facade's `sql.` keying (the default schema keeps its literal - * `__unbound__` id). + * Schema-qualified SQL view: the deserialized contract intersected with a single + * `view.namespace.` member — every schema reached by raw id + * (`view.namespace.public.table.users`), mirroring the runtime `db.enums.` + * keying exactly (the default schema keeps its literal `__unbound__` id). There + * is NO root schema-name promotion, so there is no collision with contract + * envelope fields — `view.storage` is always the contract's `storage`. Postgres + * uses this; SQLite (single default namespace) uses + * {@link buildSqlSingleNamespaceView}. */ export type SqlSchemaQualifiedView> = TContract & { - readonly namespace: NamespaceAccessor, SqlBuiltinKind>; -} & PromotedNamespaces, SqlBuiltinKind>; + readonly namespace: NamespacedEntities; +}; /** - * Builds the schema-qualified SQL view: the `namespace` accessor holds every - * schema by raw id; non-colliding schema names are promoted to the root. - * Postgres uses this; SQLite (single default namespace) uses - * {@link buildSqlSingleNamespaceView}. + * Builds the schema-qualified SQL view: attaches the namespace-keyed `namespace` + * map over the deserialized contract. Postgres uses this. */ export function buildSqlSchemaQualifiedView>( contract: TContract, ): SqlSchemaQualifiedView { - const namespaceAccessor = buildNamespaceAccessor< - NamespaceAccessor, SqlBuiltinKind> + const namespace = buildNamespacedEntities< + NamespacedEntities >(contract.storage, SQL_BUILTIN_KINDS); - return composeContractView>(contract, {}, namespaceAccessor); + return { + ...contract, + namespace, + }; } diff --git a/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts index 9ca8854a87..58212b20c9 100644 --- a/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts +++ b/packages/3-targets/3-targets/postgres/src/core/postgres-contract-view.ts @@ -8,21 +8,24 @@ import { PostgresContractSerializer } from './postgres-contract-serializer'; /** * A schema-qualified Postgres contract view: the deserialized contract - * intersected with per-schema accessors. It is substitutable for `Contract` - * (carries `storage`, `domain`, …) and also exposes each schema's entities: + * intersected with a single `namespace` member holding every schema by id. It is + * substitutable for `Contract` (carries `storage`, `domain`, …) and reaches each + * schema's entities through `view.namespace.`: * * ```ts * const view = PostgresContractView.fromJson(contractJson); - * view.public.table.users // typed table leaf in the public schema - * view.auth.table.users // the auth schema's own users table - * view.public.entries.policy.X // pack-contributed kind (RLS / #771 path) - * view.__unbound__.table.X // default schema, keyed by its raw id - * view.storage // the full contract is still present + * view.namespace.public.table.users // typed table leaf in the public schema + * view.namespace.auth.table.users // the auth schema's own users table + * view.namespace.public.entries.policy.X // pack-contributed kind (RLS / #771 path) + * view.namespace.__unbound__.table.X // default schema, keyed by its raw id + * view.storage // the full contract is still present * ``` * - * The schema keys mirror the facade's `sql.` keying exactly — the default - * schema is reachable under its literal `__unbound__` id, with no renaming. - * Each schema is its own key; there is no flat cross-namespace merge. + * This mirrors the runtime `db.enums.` keying exactly (the default schema + * keeps its literal `__unbound__` id). Schema names are NOT promoted to the + * contract root, so there is no collision with contract envelope fields — a + * schema named `storage` is `view.namespace.storage`, while `view.storage` + * stays the contract's `storage`. */ export type PostgresContractView = Contract> = SqlSchemaQualifiedView; diff --git a/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts b/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts deleted file mode 100644 index ff1be01817..0000000000 --- a/packages/3-targets/3-targets/postgres/test/fixtures/collision-contract.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { - Contract, - NamespaceId, - ProfileHashBase, - StorageHashBase, -} from '@prisma-next/contract/types'; -import type { SqlStorage } from '@prisma-next/sql-contract/types'; - -/** - * A hand-built, fully-typed Postgres contract whose namespaces include one named - * `storage` — colliding with the contract envelope's `storage` field — alongside - * a normal `public` schema. There is no committed emitted fixture with a - * `@@schema("storage")` model, so this typed `Contract` stands in to prove the - * collision-safety rule at the type level (and is reused for the runtime test). - * It is a real `Contract` — the view's type behaviour over it is - * identical to behaviour over an emitted contract of the same shape. - */ - -type IdColumn = { - readonly nativeType: 'int4'; - readonly codecId: 'pg/int4@1'; - readonly nullable: false; -}; - -type StorageSchemaTable = { - readonly secrets: { - readonly columns: { readonly id: IdColumn }; - readonly primaryKey: { readonly columns: readonly ['id'] }; - readonly uniques: readonly []; - readonly indexes: readonly []; - readonly foreignKeys: readonly []; - }; -}; - -type PublicSchemaTable = { - readonly widgets: { - readonly columns: { readonly id: IdColumn }; - readonly primaryKey: { readonly columns: readonly ['id'] }; - readonly uniques: readonly []; - readonly indexes: readonly []; - readonly foreignKeys: readonly []; - }; -}; - -export type CollisionContract = Contract< - SqlStorage & { - readonly storageHash: StorageHashBase<'sha256:collision'>; - readonly namespaces: { - readonly storage: { - readonly id: 'storage' & NamespaceId; - readonly kind: 'postgres-schema'; - readonly entries: { readonly table: StorageSchemaTable }; - }; - readonly public: { - readonly id: 'public' & NamespaceId; - readonly kind: 'postgres-schema'; - readonly entries: { readonly table: PublicSchemaTable }; - }; - }; - } -> & { - readonly target: 'postgres'; - readonly targetFamily: 'sql'; - readonly profileHash: ProfileHashBase<'sha256:collision'>; -}; - -/** - * A runtime value structurally matching {@link CollisionContract}. Hand-built - * (no serializer round-trip needed) — the view layering is structural and does - * not depend on hydrated IR-class identity for this collision check. - */ -export const collisionContractValue = { - target: 'postgres', - targetFamily: 'sql', - profileHash: 'sha256:collision', - capabilities: {}, - extensionPacks: {}, - meta: {}, - roots: {}, - domain: { namespaces: {} }, - storage: { - storageHash: 'sha256:collision', - namespaces: { - storage: { - id: 'storage', - kind: 'postgres-schema', - entries: { - table: { - secrets: { - columns: { id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false } }, - primaryKey: { columns: ['id'] }, - uniques: [], - indexes: [], - foreignKeys: [], - }, - }, - }, - }, - public: { - id: 'public', - kind: 'postgres-schema', - entries: { - table: { - widgets: { - columns: { id: { nativeType: 'int4', codecId: 'pg/int4@1', nullable: false } }, - primaryKey: { columns: ['id'] }, - uniques: [], - indexes: [], - foreignKeys: [], - }, - }, - }, - }, - }, - }, -}; diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts index 330f0eece6..1321321798 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test-d.ts @@ -1,16 +1,15 @@ import type { Contract as ContractBase } from '@prisma-next/contract/types'; import { expectTypeOf, test } from 'vitest'; import { PostgresContractView } from '../src/core/postgres-contract-view'; -import type { CollisionContract } from './fixtures/collision-contract'; import type { Contract } from './fixtures/namespaced-contract.d'; /** * Emit-then-consume type tests against a REAL emitted multi-schema Postgres * contract (`test/fixtures/namespaced-contract.d.ts`). The fixture declares the * SAME bare table `users` in BOTH `public` (column `email`) and `auth` (column - * `token`) — the discriminator that proves per-schema qualification. The - * collision tests additionally use a typed hand-built contract whose schema is - * named `storage` (see `fixtures/collision-contract.ts`). + * `token`) — the discriminator that proves per-schema qualification. Schemas are + * reached ONLY via `view.namespace.` (mirroring `db.enums.`); there is no + * root schema-name promotion. */ type CV = PostgresContractView; @@ -24,16 +23,7 @@ test('from() and fromJson() both return the view type', () => { expectTypeOf(PostgresContractView.fromJson).returns.toEqualTypeOf(); }); -test('each schema is its own key with its own table columns (root promotion)', () => { - expectTypeOf< - CV['public']['table']['users']['columns']['email']['codecId'] - >().toEqualTypeOf<'pg/text@1'>(); - expectTypeOf< - CV['auth']['table']['users']['columns']['token']['codecId'] - >().toEqualTypeOf<'pg/text@1'>(); -}); - -test('view.namespace. reaches every schema by raw id', () => { +test('view.namespace. reaches each schema with its own table columns', () => { expectTypeOf< CV['namespace']['public']['table']['users']['columns']['email']['codecId'] >().toEqualTypeOf<'pg/text@1'>(); @@ -45,69 +35,44 @@ test('view.namespace. reaches every schema by raw id', () => { test('cross-schema column access is a compile error', () => { const view = null as unknown as CV; // @ts-expect-error public.users has no `token` column (that is auth.users) - view.public.table.users.columns.token; + view.namespace.public.table.users.columns.token; // @ts-expect-error auth.users has no `email` column (that is public.users) - view.auth.table.users.columns.email; + view.namespace.auth.table.users.columns.email; }); test('a non-existent schema key is a compile error', () => { const view = null as unknown as CV; // @ts-expect-error 'marketing' is not an emitted schema - view.marketing; + view.namespace.marketing; }); test('a non-existent table name in a schema is a compile error', () => { const view = null as unknown as CV; // @ts-expect-error 'orders' is not a table in the public schema - view.public.table.orders; + view.namespace.public.table.orders; +}); + +test('schema names are NOT promoted to the contract root (no collision)', () => { + // `public` is a schema, reached via `.namespace.public`, never a root key. + type RootHasPublic = 'public' extends keyof CV ? true : false; + expectTypeOf().toEqualTypeOf(); }); test('the cross-schema foreign key on public.profile is reachable', () => { expectTypeOf< - CV['public']['table']['profile']['foreignKeys'][0]['target']['namespaceId'] + CV['namespace']['public']['table']['profile']['foreignKeys'][0]['target']['namespaceId'] >().toEqualTypeOf<'auth' & import('@prisma-next/contract/types').NamespaceId>(); }); test('valueSet slot is present per schema (none emitted, so empty maps)', () => { - expectTypeOf().toEqualTypeOf>(); - expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf>(); }); -test('view..entries excludes the built-in table and valueSet keys', () => { - type PublicEntries = CV['public']['entries']; +test('view.namespace..entries excludes the built-in table and valueSet keys', () => { + type PublicEntries = CV['namespace']['public']['entries']; type HasTable = 'table' extends keyof PublicEntries ? true : false; type HasValueSet = 'valueSet' extends keyof PublicEntries ? true : false; expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); - -// --- Collision: a schema named `storage` must not shadow the contract field --- - -type Collision = PostgresContractView; - -test('view.storage stays the contract field even when a schema is named `storage`', () => { - // The contract envelope field wins at the root: `view.storage` carries - // `.namespaces`, NOT the schema view (which would have `.table`). - expectTypeOf().toMatchTypeOf(); - type StorageHasTable = 'table' extends keyof Collision['storage'] ? true : false; - expectTypeOf().toEqualTypeOf(); -}); - -test('the `storage`-named schema is reachable via view.namespace.storage', () => { - expectTypeOf< - Collision['namespace']['storage']['table']['secrets']['columns']['id']['codecId'] - >().toEqualTypeOf<'pg/int4@1'>(); -}); - -test('a non-colliding schema (`public`) is still promoted to the root', () => { - expectTypeOf< - Collision['public']['table']['widgets']['columns']['id']['codecId'] - >().toEqualTypeOf<'pg/int4@1'>(); - expectTypeOf< - Collision['namespace']['public']['table']['widgets']['columns']['id']['codecId'] - >().toEqualTypeOf<'pg/int4@1'>(); -}); - -test('the collision view is still assignable to Contract', () => { - expectTypeOf().toMatchTypeOf(); -}); diff --git a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts index 3240508d5f..0652de2bfa 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-contract-view.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; import { PostgresContractSerializer } from '../src/core/postgres-contract-serializer'; import { PostgresContractView } from '../src/core/postgres-contract-view'; -import { type CollisionContract, collisionContractValue } from './fixtures/collision-contract'; import type { Contract } from './fixtures/namespaced-contract.d'; import contractJson from './fixtures/namespaced-contract.json' with { type: 'json' }; @@ -19,51 +18,53 @@ describe('PostgresContractView', () => { expect(view.roots).toBe(contract.roots); }); - it('keys each schema separately with its own tables', () => { + it('keys each schema separately under view.namespace with its own tables', () => { const view = PostgresContractView.from(contract); - expect(view.public.table.users).toBeDefined(); - expect(view.auth.table.users).toBeDefined(); - expect(Object.keys(view.public.table.users.columns).sort()).toEqual(['email', 'id']); - expect(Object.keys(view.auth.table.users.columns).sort()).toEqual(['id', 'token']); + expect(view.namespace.public.table.users).toBeDefined(); + expect(view.namespace.auth.table.users).toBeDefined(); + expect(Object.keys(view.namespace.public.table.users.columns).sort()).toEqual(['email', 'id']); + expect(Object.keys(view.namespace.auth.table.users.columns).sort()).toEqual(['id', 'token']); }); - it('view..table. returns the same entity object as the raw contract', () => { + it('view.namespace..table. returns the same entity object as the raw contract', () => { const view = PostgresContractView.from(contract); - expect(view.public.table.users).toBe( + expect(view.namespace.public.table.users).toBe( contract.storage.namespaces['public']?.entries.table?.users, ); - expect(view.auth.table.users).toBe(contract.storage.namespaces['auth']?.entries.table?.users); + expect(view.namespace.auth.table.users).toBe( + contract.storage.namespaces['auth']?.entries.table?.users, + ); }); - it('view.namespace. reaches every schema by raw id', () => { + it('schema names are NOT promoted to the contract root', () => { const view = PostgresContractView.from(contract); - expect(view.namespace.public.table.users).toBe(view.public.table.users); - expect(view.namespace.auth.table.users).toBe(view.auth.table.users); + expect((view as unknown as Record)['public']).toBeUndefined(); + expect((view as unknown as Record)['auth']).toBeUndefined(); }); - it('view..valueSet is present and empty (no value sets emitted)', () => { + it('view.namespace..valueSet is present and empty (no value sets emitted)', () => { const view = PostgresContractView.from(contract); - expect(view.public.valueSet).toEqual({}); - expect(view.auth.valueSet).toEqual({}); + expect(view.namespace.public.valueSet).toEqual({}); + expect(view.namespace.auth.valueSet).toEqual({}); }); - it('view..entries excludes the built-in table and valueSet keys', () => { + it('view.namespace..entries excludes the built-in table and valueSet keys', () => { const view = PostgresContractView.from(contract); - expect(Object.keys(view.public.entries)).not.toContain('table'); - expect(Object.keys(view.public.entries)).not.toContain('valueSet'); + expect(Object.keys(view.namespace.public.entries)).not.toContain('table'); + expect(Object.keys(view.namespace.public.entries)).not.toContain('valueSet'); }); it('fromJson() deserializes and wraps in one call', () => { const view = PostgresContractView.fromJson(contractJson); - expect(view.public.table.users).toBeDefined(); - expect(view.auth.table.users).toBeDefined(); + expect(view.namespace.public.table.users).toBeDefined(); + expect(view.namespace.auth.table.users).toBeDefined(); expect(view.storage.storageHash).toBe(contract.storage.storageHash); }); - it('the default __unbound__ schema is keyed by its raw id (mirrors the facade)', () => { + it('the default __unbound__ schema is keyed by its raw id under view.namespace', () => { // Mirror the facade's keying: the default schema is reachable under its raw - // `__unbound__` id, not a renamed key. Hand-built since the committed - // namespaced fixture uses only named schemas. + // `__unbound__` id. Hand-built since the committed namespaced fixture uses + // only named schemas. const withDefault = { ...contract, storage: { @@ -79,14 +80,15 @@ describe('PostgresContractView', () => { }, } as unknown as Contract; - const view = PostgresContractView.from(withDefault) as unknown as Record< - string, - { table: Record } - >; - expect(view['__unbound__']?.table['widgets']).toBeDefined(); + const view = PostgresContractView.from(withDefault); + expect( + (view.namespace as Record }>)['__unbound__']?.table[ + 'widgets' + ], + ).toBeDefined(); }); - it('view..entries exposes pack-contributed kinds', () => { + it('view.namespace..entries exposes pack-contributed kinds', () => { // RLS `policy` isn't in a committed fixture yet, so hand-build a contract // with a pack-contributed `policy` kind under the public schema. const fakePolicy = { name: 'read_all' }; @@ -108,35 +110,8 @@ describe('PostgresContractView', () => { } as unknown as Contract; const view = PostgresContractView.from(withPolicy); - expect((view.public.entries as Record)['policy']).toEqual({ + expect((view.namespace.public.entries as Record)['policy']).toEqual({ readAll: fakePolicy, }); }); - - describe('schema-name collision with a contract field', () => { - const collision = collisionContractValue as unknown as CollisionContract; - - it('view.storage stays the contract field (not the schema named `storage`)', () => { - const view = PostgresContractView.from(collision); - expect(view.storage).toBe(collision.storage); - expect(view.storage.namespaces).toBeDefined(); - // The contract field has no `.table` — the schema view would. - expect((view.storage as unknown as Record)['table']).toBeUndefined(); - }); - - it('the `storage`-named schema is reachable via view.namespace.storage', () => { - const view = PostgresContractView.from(collision); - expect(view.namespace.storage.table.secrets).toBe( - collision.storage.namespaces.storage.entries.table.secrets, - ); - }); - - it('a non-colliding schema (`public`) is promoted to the root AND under namespace', () => { - const view = PostgresContractView.from(collision); - expect(view.public.table.widgets).toBe( - collision.storage.namespaces.public.entries.table.widgets, - ); - expect(view.namespace.public.table.widgets).toBe(view.public.table.widgets); - }); - }); }); From 0b068ed575b651aecbc29158cb3debe84bb74b3b Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 29 Jun 2026 12:53:14 +0200 Subject: [PATCH 05/12] feat(tml-2892): Migration base derives describe() from contract JSON + exposes views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../app/20260513T0505_initial/migration.ts | 31 +- .../migration.ts | 76 +- .../migration/src/exports/migration.ts | 1 + .../3-tooling/migration/src/migration-base.ts | 69 +- .../migration/test/migration-base.test.ts | 57 + .../9-family/src/core/mongo-migration.ts | 51 +- .../test/fixtures/migration-end-contract.d.ts | 1322 ++++++++++++++++ .../test/fixtures/migration-end-contract.json | 1407 +++++++++++++++++ .../9-family/test/mongo-migration.test.ts | 73 + .../9-family/tsconfig.test.json | 3 +- 10 files changed, 3020 insertions(+), 70 deletions(-) create mode 100644 packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.d.ts create mode 100644 packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.json create mode 100644 packages/2-mongo-family/9-family/test/mongo-migration.test.ts diff --git a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts index be82cf00e0..7fddba6c19 100755 --- a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts @@ -1,31 +1,22 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createCollection, createIndex } from '@prisma-next/target-mongo/migration'; -import type { Contract } from './end-contract'; +import type { Contract as End } from './end-contract'; import endContractJson from './end-contract.json' with { type: 'json' }; -const endContract = MongoContractView.fromJson(endContractJson); - -const COLLECTIONS = endContract.collection; -const CARTS_VALIDATOR = COLLECTIONS.carts.validator; -const EVENTS_VALIDATOR = COLLECTIONS.events.validator; -const INVOICES_VALIDATOR = COLLECTIONS.invoices.validator; -const LOCATIONS_VALIDATOR = COLLECTIONS.locations.validator; -const ORDERS_VALIDATOR = COLLECTIONS.orders.validator; -const PRODUCTS_VALIDATOR = COLLECTIONS.products.validator; -const USERS_VALIDATOR = COLLECTIONS.users.validator; - -class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:8ee1e7ce30ed334572583d826d9c41388c46f7db82ae2352c3a3fccf1de7cbab', - }; - } +class M extends Migration { + override readonly endContractJson = endContractJson; override get operations() { + const COLLECTIONS = this.endContract.collection; + const CARTS_VALIDATOR = COLLECTIONS.carts.validator; + const EVENTS_VALIDATOR = COLLECTIONS.events.validator; + const INVOICES_VALIDATOR = COLLECTIONS.invoices.validator; + const LOCATIONS_VALIDATOR = COLLECTIONS.locations.validator; + const ORDERS_VALIDATOR = COLLECTIONS.orders.validator; + const PRODUCTS_VALIDATOR = COLLECTIONS.products.validator; + const USERS_VALIDATOR = COLLECTIONS.users.validator; return [ createCollection('carts', { validator: { $jsonSchema: CARTS_VALIDATOR.jsonSchema }, diff --git a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts index 0f0c47d89f..40f0de9659 100755 --- a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts @@ -1,6 +1,5 @@ #!/usr/bin/env -S node import { MigrationCLI } from '@prisma-next/cli/migration-cli'; -import { MongoContractView } from '@prisma-next/family-mongo/ir'; import { Migration } from '@prisma-next/family-mongo/migration'; import { AggregateCommand, @@ -11,44 +10,23 @@ import { RawUpdateManyCommand, } from '@prisma-next/mongo-query-ast/execution'; import { dataTransform, setValidation } from '@prisma-next/target-mongo/migration'; -import type { Contract } from './end-contract'; +import type { Contract as End } from './end-contract'; import endContractJson from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContractJson from './start-contract.json' with { type: 'json' }; -const endContract = MongoContractView.fromJson(endContractJson); - -const STORAGE_HASH = endContract.storage.storageHash; - -// `migration new` records the contract delta as `from` → `to` but produces an -// empty `operations` array; the author is responsible for declaring the work -// that bridges the two contract states. This migration's contract delta adds -// `embedding` and `status` fields to `products` (with `status` becoming -// required via its `@default("active")`), so two ops are needed: -// -// 1. `setValidation` — refresh the live `$jsonSchema` so it includes the new -// fields. Without this, `db verify` would fail with `VALIDATOR_MISMATCH` -// after this migration applies because the live validator (still the -// state-1 shape from migration 1's `createCollection`) wouldn't match the -// contract-derived expected validator for state 3. -// -// 2. `dataTransform` — backfill `status: "active"` on pre-existing products -// so they satisfy the new `required: [..., "status", ...]` rule. -// -// The validator is sourced from `end-contract.json` so the op stays in sync -// with the contract if the chain is ever re-emitted. -const PRODUCTS_VALIDATOR = endContract.collection.products.validator; - -function existingProductsWithoutStatus(): MongoQueryPlan { +function existingProductsWithoutStatus(storageHash: string): MongoQueryPlan { return { collection: 'products', command: new AggregateCommand('products', [ new MongoMatchStage(new MongoExistsExpr('status', false)), new MongoLimitStage(1), ]), - meta: { target: 'mongo', storageHash: STORAGE_HASH, lane: 'mongo-pipeline' }, + meta: { target: 'mongo', storageHash, lane: 'mongo-pipeline' }, }; } -function backfillRun(): MongoQueryPlan { +function backfillRun(storageHash: string): MongoQueryPlan { return { collection: 'products', // Raw command form: the typed query-builder (`mongoQuery(...).updateMany(...)`) @@ -61,28 +39,42 @@ function backfillRun(): MongoQueryPlan { { status: { $exists: false } }, { $set: { status: 'active' } }, ), - meta: { target: 'mongo', storageHash: STORAGE_HASH, lane: 'mongo-raw' }, + meta: { target: 'mongo', storageHash, lane: 'mongo-raw' }, }; } -class BackfillProductStatus extends Migration { - override describe() { - return { - from: 'sha256:059f3f35403c5a7a90851c23f1028e16d5250630f8a82fba33053e9a50534589', - to: 'sha256:71f1cc5c3f4de1ea7c9c8426fde682cd78c7c005f6688f58c2d9d6ddd8b2284c', - labels: ['backfill-product-status'], - }; - } +class BackfillProductStatus extends Migration { + override readonly startContractJson = startContractJson; + override readonly endContractJson = endContractJson; + // `migration new` records the contract delta as `from` → `to` but produces an + // empty `operations` array; the author is responsible for declaring the work + // that bridges the two contract states. This migration's contract delta adds + // `embedding` and `status` fields to `products` (with `status` becoming + // required via its `@default("active")`), so two ops are needed: + // + // 1. `setValidation` — refresh the live `$jsonSchema` so it includes the new + // fields. Without this, `db verify` would fail with `VALIDATOR_MISMATCH` + // after this migration applies because the live validator (still the + // state-1 shape from migration 1's `createCollection`) wouldn't match the + // contract-derived expected validator for state 3. + // + // 2. `dataTransform` — backfill `status: "active"` on pre-existing products + // so they satisfy the new `required: [..., "status", ...]` rule. + // + // The validator is sourced from the end-state contract view so the op stays + // in sync with the contract if the chain is ever re-emitted. override get operations() { + const storageHash = this.endContract.storage.storageHash; + const productsValidator = this.endContract.collection.products.validator; return [ - setValidation('products', PRODUCTS_VALIDATOR.jsonSchema, { - validationLevel: PRODUCTS_VALIDATOR.validationLevel, - validationAction: PRODUCTS_VALIDATOR.validationAction, + setValidation('products', productsValidator.jsonSchema, { + validationLevel: productsValidator.validationLevel, + validationAction: productsValidator.validationAction, }), dataTransform('backfill-product-status', { - check: { source: existingProductsWithoutStatus }, - run: backfillRun, + check: { source: () => existingProductsWithoutStatus(storageHash) }, + run: () => backfillRun(storageHash), }), ]; } diff --git a/packages/1-framework/3-tooling/migration/src/exports/migration.ts b/packages/1-framework/3-tooling/migration/src/exports/migration.ts index 6581ee51c5..4f2e3ecb8f 100644 --- a/packages/1-framework/3-tooling/migration/src/exports/migration.ts +++ b/packages/1-framework/3-tooling/migration/src/exports/migration.ts @@ -1,5 +1,6 @@ export { buildMigrationArtifacts, + type ContractHashInput, isDirectEntrypoint, Migration, type MigrationArtifacts, diff --git a/packages/1-framework/3-tooling/migration/src/migration-base.ts b/packages/1-framework/3-tooling/migration/src/migration-base.ts index 4192311e47..c8291839b2 100644 --- a/packages/1-framework/3-tooling/migration/src/migration-base.ts +++ b/packages/1-framework/3-tooling/migration/src/migration-base.ts @@ -1,5 +1,6 @@ import { realpathSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; +import type { Contract } from '@prisma-next/contract/types'; import type { ControlStack, MigrationPlan, @@ -18,6 +19,18 @@ export interface MigrationMeta { readonly to: string; } +/** + * The minimal structural shape `describe()` reads off a migration's contract + * JSON input: just `storage.storageHash`. A raw `contract.json` import + * structurally satisfies this, so subclasses assign their JSON imports to + * `startContractJson`/`endContractJson` WITHOUT a cast. The full `Start`/`End` + * contract typing is applied downstream in the family bases' view getters (via + * `ContractView.fromJson<…>`), not on these raw fields. + */ +export interface ContractHashInput { + readonly storage: { readonly storageHash: string }; +} + // `from` rejects empty strings to mirror `MigrationMetadataSchema` in // `./io.ts`. Without this match, an authored migration could `describe()` with // `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have @@ -33,18 +46,47 @@ const MigrationMetaSchema = type({ * * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the * runner can consume it directly via `targetId`, `operations`, `origin`, and - * `destination`. The metadata-shaped inputs come from `describe()`, which - * every migration must implement — `migration.json` is required for a - * migration to be valid. + * `destination`. + * + * The from/to identities come from `describe()`. A migration provides them in + * one of two ways: + * - **Contract-derived (default):** assign the committed `start-contract.json` + * / `end-contract.json` imports to `startContractJson` / `endContractJson`; + * the concrete `describe()` below derives `to`/`from` from their + * `storage.storageHash`. The family bases additionally expose typed view + * getters (`startContract` / `endContract`) over the same JSON. + * - **Override (e.g. extension migrations that carry no contract):** override + * `describe()` directly; the override wins and the JSON fields are unused. + * + * The `Start` / `End` generics carry each migration's precise contract types so + * the family-base view getters resolve to fully-typed views. */ export abstract class Migration< _TOperation extends MigrationPlanOperation = MigrationPlanOperation, TFamilyId extends string = string, TTargetId extends string = string, + _Start extends Contract = Contract, + _End extends Contract = Contract, > implements MigrationPlan { abstract readonly targetId: string; + /** + * The migration's end-state contract JSON (the committed `end-contract.json` + * import). When set, the derived `describe()` reads `to` from its + * `storage.storageHash`. Family bases build the typed `endContract` view from + * it. Optional so `describe()`-overriding migrations (no contract) compile. + */ + readonly endContractJson?: ContractHashInput; + + /** + * The migration's start-state contract JSON (the committed + * `start-contract.json` import). Absent for a baseline migration (`from` + * derives to `null`). Family bases build the typed `startContract` view from + * it. + */ + readonly startContractJson?: ContractHashInput; + /** * Assembled `ControlStack` injected by the orchestrator (`runMigration`). * @@ -71,10 +113,25 @@ export abstract class Migration< /** * Metadata inputs used to build `migration.json` and to derive the plan's - * origin/destination identities. Every migration must provide this — - * omitting it would produce an invalid on-disk migration package. + * origin/destination identities. + * + * Default derivation: `to = endContractJson.storage.storageHash`, + * `from = startContractJson?.storage.storageHash ?? null`. A migration that + * carries no contract JSON (e.g. an extension migration) must override this; + * otherwise it throws, since `migration.json` requires a `to` identity. */ - abstract describe(): MigrationMeta; + describe(): MigrationMeta { + const end = this.endContractJson; + if (end === undefined) { + throw new Error( + 'Migration.describe(): provide endContractJson or override describe() — a migration needs a destination contract hash.', + ); + } + return { + from: this.startContractJson?.storage.storageHash ?? null, + to: end.storage.storageHash, + }; + } get origin(): { readonly storageHash: string } | null { const from = this.describe().from; diff --git a/packages/1-framework/3-tooling/migration/test/migration-base.test.ts b/packages/1-framework/3-tooling/migration/test/migration-base.test.ts index 7c87557f33..1727ed9167 100644 --- a/packages/1-framework/3-tooling/migration/test/migration-base.test.ts +++ b/packages/1-framework/3-tooling/migration/test/migration-base.test.ts @@ -76,6 +76,63 @@ describe('Migration', async () => { }); }); + describe('derived describe() from contract JSON', async () => { + const endJson = { storage: { storageHash: 'sha256:endhash' } }; + const startJson = { storage: { storageHash: 'sha256:starthash' } }; + + it('derives to from endContractJson.storage.storageHash and from:null when no start', async () => { + class M extends Migration { + readonly targetId = 'test'; + override readonly endContractJson = endJson; + override get operations() { + return []; + } + } + const m = new M(); + expect(m.describe()).toEqual({ from: null, to: 'sha256:endhash' }); + expect(m.origin).toBeNull(); + expect(m.destination).toEqual({ storageHash: 'sha256:endhash' }); + }); + + it('derives from from startContractJson.storage.storageHash when present', async () => { + class M extends Migration { + readonly targetId = 'test'; + override readonly startContractJson = startJson; + override readonly endContractJson = endJson; + override get operations() { + return []; + } + } + const m = new M(); + expect(m.describe()).toEqual({ from: 'sha256:starthash', to: 'sha256:endhash' }); + expect(m.origin).toEqual({ storageHash: 'sha256:starthash' }); + }); + + it('throws a clear error when neither endContractJson nor a describe() override is present', async () => { + class M extends Migration { + readonly targetId = 'test'; + override get operations() { + return []; + } + } + expect(() => new M().describe()).toThrow(/endContractJson or override describe\(\)/); + }); + + it('a describe() override still wins over the derived default', async () => { + class M extends Migration { + readonly targetId = 'test'; + override readonly endContractJson = endJson; + override get operations() { + return []; + } + override describe() { + return { from: null, to: 'sha256:overridden' }; + } + } + expect(new M().describe()).toEqual({ from: null, to: 'sha256:overridden' }); + }); + }); + describe('constructor accepts and stores a ControlStack', async () => { /** * The constructor injection contract is that a subclass (e.g. diff --git a/packages/2-mongo-family/9-family/src/core/mongo-migration.ts b/packages/2-mongo-family/9-family/src/core/mongo-migration.ts index c19662930e..0a4131a241 100644 --- a/packages/2-mongo-family/9-family/src/core/mongo-migration.ts +++ b/packages/2-mongo-family/9-family/src/core/mongo-migration.ts @@ -1,5 +1,7 @@ import { Migration } from '@prisma-next/migration-tools/migration'; +import type { MongoContract } from '@prisma-next/mongo-contract'; import type { AnyMongoMigrationOperation } from '@prisma-next/mongo-query-ast/control'; +import { MongoContractView } from './ir/mongo-contract-view'; /** * Family-owned base class for Mongo migrations. @@ -14,7 +16,54 @@ import type { AnyMongoMigrationOperation } from '@prisma-next/mongo-query-ast/co * so subclasses can return a mix of schema operations (e.g. `createIndex`, * `setValidation`) and data-transform operations (e.g. `dataTransform`). * Mirrors the generic parameter used by `PlannerProducedMongoMigration`. + * + * Binds the framework base's `Start` / `End` contract generics so a subclass + * that assigns its `start-contract.json` / `end-contract.json` imports gets + * fully-typed view accessors: `this.endContract` is a `MongoContractView` + * (and `this.startContract` a `MongoContractView | null`), built lazily + * from those JSON fields. The framework base derives `describe()` from the same + * JSON. View getters live on the family base (not the framework base) because + * `MongoContractView`'s shape is Mongo-specific. */ -export abstract class MongoMigration extends Migration { +export abstract class MongoMigration< + Start extends MongoContract = MongoContract, + End extends MongoContract = MongoContract, +> extends Migration { readonly targetId = 'mongo' as const; + + #endContract?: MongoContractView; + #startContract?: MongoContractView | null; + + /** + * The typed, namespace-unwrapped view over this migration's end-state + * contract — `this.endContract.collection..validator`, etc. Lazily + * built from `endContractJson` and memoized. Throws if no `endContractJson` + * was provided (a `describe()`-overriding migration with no contract has no + * view to expose). + */ + get endContract(): MongoContractView { + if (this.#endContract === undefined) { + if (this.endContractJson === undefined) { + throw new Error( + 'MongoMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', + ); + } + this.#endContract = MongoContractView.fromJson(this.endContractJson); + } + return this.#endContract; + } + + /** + * The typed view over this migration's start-state contract, or `null` for a + * baseline migration (no `startContractJson`). Lazily built and memoized. + */ + get startContract(): MongoContractView | null { + if (this.#startContract === undefined) { + this.#startContract = + this.startContractJson === undefined + ? null + : MongoContractView.fromJson(this.startContractJson); + } + return this.#startContract; + } } diff --git a/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.d.ts b/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.d.ts new file mode 100644 index 0000000000..2b91b05055 --- /dev/null +++ b/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.d.ts @@ -0,0 +1,1322 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { CodecTypes as MongoCodecTypes, Vector } from '@prisma-next/adapter-mongo/codec-types'; + +import type { + MongoCollection, + MongoContractWithTypeMaps, + MongoTypeMaps, +} from '@prisma-next/mongo-contract'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:8ee1e7ce30ed334572583d826d9c41388c46f7db82ae2352c3a3fccf1de7cbab'>; +export type ExecutionHash = ExecutionHashBase; +export type ProfileHash = + ProfileHashBase<'sha256:cca47cfb902adf4e15c2f277dd98af4aff64a3a2c010b49ace1c897de1cc4510'>; + +export type CodecTypes = MongoCodecTypes; + +export type PriceOutput = { + readonly amount: CodecTypes['mongo/double@1']['output']; + readonly currency: CodecTypes['mongo/string@1']['output']; +}; +export type PriceInput = { + readonly amount: CodecTypes['mongo/double@1']['input']; + readonly currency: CodecTypes['mongo/string@1']['input']; +}; +export type ImageOutput = { readonly url: CodecTypes['mongo/string@1']['output'] }; +export type ImageInput = { readonly url: CodecTypes['mongo/string@1']['input'] }; +export type AddressOutput = { + readonly streetAndNumber: CodecTypes['mongo/string@1']['output']; + readonly city: CodecTypes['mongo/string@1']['output']; + readonly postalCode: CodecTypes['mongo/string@1']['output']; + readonly country: CodecTypes['mongo/string@1']['output']; +}; +export type AddressInput = { + readonly streetAndNumber: CodecTypes['mongo/string@1']['input']; + readonly city: CodecTypes['mongo/string@1']['input']; + readonly postalCode: CodecTypes['mongo/string@1']['input']; + readonly country: CodecTypes['mongo/string@1']['input']; +}; +export type CartItemOutput = { + readonly productId: CodecTypes['mongo/string@1']['output']; + readonly name: CodecTypes['mongo/string@1']['output']; + readonly brand: CodecTypes['mongo/string@1']['output']; + readonly amount: CodecTypes['mongo/int32@1']['output']; + readonly price: PriceOutput; + readonly image: ImageOutput; +}; +export type CartItemInput = { + readonly productId: CodecTypes['mongo/string@1']['input']; + readonly name: CodecTypes['mongo/string@1']['input']; + readonly brand: CodecTypes['mongo/string@1']['input']; + readonly amount: CodecTypes['mongo/int32@1']['input']; + readonly price: PriceInput; + readonly image: ImageInput; +}; +export type OrderLineItemOutput = { + readonly productId: CodecTypes['mongo/string@1']['output']; + readonly name: CodecTypes['mongo/string@1']['output']; + readonly brand: CodecTypes['mongo/string@1']['output']; + readonly amount: CodecTypes['mongo/int32@1']['output']; + readonly price: PriceOutput; + readonly image: ImageOutput; +}; +export type OrderLineItemInput = { + readonly productId: CodecTypes['mongo/string@1']['input']; + readonly name: CodecTypes['mongo/string@1']['input']; + readonly brand: CodecTypes['mongo/string@1']['input']; + readonly amount: CodecTypes['mongo/int32@1']['input']; + readonly price: PriceInput; + readonly image: ImageInput; +}; +export type StatusEntryOutput = { + readonly status: CodecTypes['mongo/string@1']['output']; + readonly timestamp: CodecTypes['mongo/date@1']['output']; +}; +export type StatusEntryInput = { + readonly status: CodecTypes['mongo/string@1']['input']; + readonly timestamp: CodecTypes['mongo/date@1']['input']; +}; +export type InvoiceLineItemOutput = { + readonly name: CodecTypes['mongo/string@1']['output']; + readonly amount: CodecTypes['mongo/int32@1']['output']; + readonly unitPrice: CodecTypes['mongo/double@1']['output']; + readonly lineTotal: CodecTypes['mongo/double@1']['output']; +}; +export type InvoiceLineItemInput = { + readonly name: CodecTypes['mongo/string@1']['input']; + readonly amount: CodecTypes['mongo/int32@1']['input']; + readonly unitPrice: CodecTypes['mongo/double@1']['input']; + readonly lineTotal: CodecTypes['mongo/double@1']['input']; +}; +export type FieldOutputTypes = { + readonly __unbound__: { + readonly AddToCartEvent: { + readonly productId: CodecTypes['mongo/string@1']['output']; + readonly brand: CodecTypes['mongo/string@1']['output']; + }; + readonly Cart: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly userId: CodecTypes['mongo/objectId@1']['output']; + readonly items: ReadonlyArray; + }; + readonly Event: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly userId: CodecTypes['mongo/string@1']['output']; + readonly sessionId: CodecTypes['mongo/string@1']['output']; + readonly type: CodecTypes['mongo/string@1']['output']; + readonly timestamp: CodecTypes['mongo/date@1']['output']; + }; + readonly Invoice: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly orderId: CodecTypes['mongo/objectId@1']['output']; + readonly items: ReadonlyArray; + readonly subtotal: CodecTypes['mongo/double@1']['output']; + readonly tax: CodecTypes['mongo/double@1']['output']; + readonly total: CodecTypes['mongo/double@1']['output']; + readonly issuedAt: CodecTypes['mongo/date@1']['output']; + }; + readonly Location: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly name: CodecTypes['mongo/string@1']['output']; + readonly streetAndNumber: CodecTypes['mongo/string@1']['output']; + readonly city: CodecTypes['mongo/string@1']['output']; + readonly postalCode: CodecTypes['mongo/string@1']['output']; + readonly country: CodecTypes['mongo/string@1']['output']; + }; + readonly Order: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly userId: CodecTypes['mongo/objectId@1']['output']; + readonly items: ReadonlyArray; + readonly shippingAddress: CodecTypes['mongo/string@1']['output']; + readonly type: CodecTypes['mongo/string@1']['output']; + readonly statusHistory: ReadonlyArray; + }; + readonly Product: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly name: CodecTypes['mongo/string@1']['output']; + readonly brand: CodecTypes['mongo/string@1']['output']; + readonly code: CodecTypes['mongo/string@1']['output']; + readonly description: CodecTypes['mongo/string@1']['output']; + readonly primaryCategory: CodecTypes['mongo/string@1']['output']; + readonly subCategory: CodecTypes['mongo/string@1']['output']; + readonly articleType: CodecTypes['mongo/string@1']['output']; + readonly price: PriceOutput; + readonly image: ImageOutput; + }; + readonly SearchEvent: { readonly query: CodecTypes['mongo/string@1']['output'] }; + readonly User: { + readonly _id: CodecTypes['mongo/objectId@1']['output']; + readonly name: CodecTypes['mongo/string@1']['output']; + readonly email: CodecTypes['mongo/string@1']['output']; + readonly address: AddressOutput | null; + }; + readonly ViewProductEvent: { + readonly productId: CodecTypes['mongo/string@1']['output']; + readonly subCategory: CodecTypes['mongo/string@1']['output']; + readonly brand: CodecTypes['mongo/string@1']['output']; + readonly exitMethod: CodecTypes['mongo/string@1']['output'] | null; + }; + }; +}; +export type FieldInputTypes = { + readonly __unbound__: { + readonly AddToCartEvent: { + readonly productId: CodecTypes['mongo/string@1']['input']; + readonly brand: CodecTypes['mongo/string@1']['input']; + }; + readonly Cart: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly userId: CodecTypes['mongo/objectId@1']['input']; + readonly items: ReadonlyArray; + }; + readonly Event: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly userId: CodecTypes['mongo/string@1']['input']; + readonly sessionId: CodecTypes['mongo/string@1']['input']; + readonly type: CodecTypes['mongo/string@1']['input']; + readonly timestamp: CodecTypes['mongo/date@1']['input']; + }; + readonly Invoice: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly orderId: CodecTypes['mongo/objectId@1']['input']; + readonly items: ReadonlyArray; + readonly subtotal: CodecTypes['mongo/double@1']['input']; + readonly tax: CodecTypes['mongo/double@1']['input']; + readonly total: CodecTypes['mongo/double@1']['input']; + readonly issuedAt: CodecTypes['mongo/date@1']['input']; + }; + readonly Location: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly name: CodecTypes['mongo/string@1']['input']; + readonly streetAndNumber: CodecTypes['mongo/string@1']['input']; + readonly city: CodecTypes['mongo/string@1']['input']; + readonly postalCode: CodecTypes['mongo/string@1']['input']; + readonly country: CodecTypes['mongo/string@1']['input']; + }; + readonly Order: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly userId: CodecTypes['mongo/objectId@1']['input']; + readonly items: ReadonlyArray; + readonly shippingAddress: CodecTypes['mongo/string@1']['input']; + readonly type: CodecTypes['mongo/string@1']['input']; + readonly statusHistory: ReadonlyArray; + }; + readonly Product: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly name: CodecTypes['mongo/string@1']['input']; + readonly brand: CodecTypes['mongo/string@1']['input']; + readonly code: CodecTypes['mongo/string@1']['input']; + readonly description: CodecTypes['mongo/string@1']['input']; + readonly primaryCategory: CodecTypes['mongo/string@1']['input']; + readonly subCategory: CodecTypes['mongo/string@1']['input']; + readonly articleType: CodecTypes['mongo/string@1']['input']; + readonly price: PriceInput; + readonly image: ImageInput; + }; + readonly SearchEvent: { readonly query: CodecTypes['mongo/string@1']['input'] }; + readonly User: { + readonly _id: CodecTypes['mongo/objectId@1']['input']; + readonly name: CodecTypes['mongo/string@1']['input']; + readonly email: CodecTypes['mongo/string@1']['input']; + readonly address: AddressInput | null; + }; + readonly ViewProductEvent: { + readonly productId: CodecTypes['mongo/string@1']['input']; + readonly subCategory: CodecTypes['mongo/string@1']['input']; + readonly brand: CodecTypes['mongo/string@1']['input']; + readonly exitMethod: CodecTypes['mongo/string@1']['input'] | null; + }; + }; +}; +export type TypeMaps = MongoTypeMaps; + +type ContractBase = Omit< + ContractType<{ + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'mongo-database'; + readonly entries: { + readonly collection: { + readonly carts: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'userId'; readonly direction: 1 }]; + readonly unique: true; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'objectId' }; + readonly items: { + readonly bsonType: 'array'; + readonly items: { + readonly bsonType: 'object'; + readonly properties: { + readonly productId: { readonly bsonType: 'string' }; + readonly name: { readonly bsonType: 'string' }; + readonly brand: { readonly bsonType: 'string' }; + readonly amount: { readonly bsonType: 'int' }; + readonly price: { + readonly bsonType: 'object'; + readonly properties: { + readonly amount: { readonly bsonType: 'double' }; + readonly currency: { readonly bsonType: 'string' }; + }; + readonly additionalProperties: false; + readonly required: readonly ['amount', 'currency']; + }; + readonly image: { + readonly bsonType: 'object'; + readonly properties: { readonly url: { readonly bsonType: 'string' } }; + readonly additionalProperties: false; + readonly required: readonly ['url']; + }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + 'amount', + 'brand', + 'image', + 'name', + 'price', + 'productId', + ]; + }; + }; + }; + readonly additionalProperties: false; + readonly required: readonly ['_id', 'items', 'userId']; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly events: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [ + { readonly field: 'userId'; readonly direction: 1 }, + { readonly field: 'timestamp'; readonly direction: -1 }, + ]; + }, + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'timestamp'; readonly direction: 1 }]; + readonly expireAfterSeconds: 7776000; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'string' }; + readonly sessionId: { readonly bsonType: 'string' }; + readonly type: { readonly bsonType: 'string' }; + readonly timestamp: { readonly bsonType: 'date' }; + }; + readonly required: readonly ['_id', 'sessionId', 'timestamp', 'type', 'userId']; + readonly oneOf: readonly [ + { + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'string' }; + readonly sessionId: { readonly bsonType: 'string' }; + readonly type: { readonly enum: readonly ['view-product'] }; + readonly timestamp: { readonly bsonType: 'date' }; + readonly productId: { readonly bsonType: 'string' }; + readonly subCategory: { readonly bsonType: 'string' }; + readonly brand: { readonly bsonType: 'string' }; + readonly exitMethod: { readonly bsonType: readonly ['null', 'string'] }; + }; + readonly required: readonly ['brand', 'productId', 'subCategory', 'type']; + readonly additionalProperties: false; + }, + { + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'string' }; + readonly sessionId: { readonly bsonType: 'string' }; + readonly type: { readonly enum: readonly ['search'] }; + readonly timestamp: { readonly bsonType: 'date' }; + readonly query: { readonly bsonType: 'string' }; + }; + readonly required: readonly ['query', 'type']; + readonly additionalProperties: false; + }, + { + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'string' }; + readonly sessionId: { readonly bsonType: 'string' }; + readonly type: { readonly enum: readonly ['add-to-cart'] }; + readonly timestamp: { readonly bsonType: 'date' }; + readonly productId: { readonly bsonType: 'string' }; + readonly brand: { readonly bsonType: 'string' }; + }; + readonly required: readonly ['brand', 'productId', 'type']; + readonly additionalProperties: false; + }, + ]; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly invoices: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'orderId'; readonly direction: 1 }]; + }, + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'issuedAt'; readonly direction: -1 }]; + readonly sparse: true; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly orderId: { readonly bsonType: 'objectId' }; + readonly items: { + readonly bsonType: 'array'; + readonly items: { + readonly bsonType: 'object'; + readonly properties: { + readonly name: { readonly bsonType: 'string' }; + readonly amount: { readonly bsonType: 'int' }; + readonly unitPrice: { readonly bsonType: 'double' }; + readonly lineTotal: { readonly bsonType: 'double' }; + }; + readonly additionalProperties: false; + readonly required: readonly ['amount', 'lineTotal', 'name', 'unitPrice']; + }; + }; + readonly subtotal: { readonly bsonType: 'double' }; + readonly tax: { readonly bsonType: 'double' }; + readonly total: { readonly bsonType: 'double' }; + readonly issuedAt: { readonly bsonType: 'date' }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + '_id', + 'issuedAt', + 'items', + 'orderId', + 'subtotal', + 'tax', + 'total', + ]; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly locations: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [ + { readonly field: 'city'; readonly direction: 1 }, + { readonly field: 'country'; readonly direction: 1 }, + ]; + readonly collation: { readonly locale: 'en'; readonly strength: 2 }; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly name: { readonly bsonType: 'string' }; + readonly streetAndNumber: { readonly bsonType: 'string' }; + readonly city: { readonly bsonType: 'string' }; + readonly postalCode: { readonly bsonType: 'string' }; + readonly country: { readonly bsonType: 'string' }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + '_id', + 'city', + 'country', + 'name', + 'postalCode', + 'streetAndNumber', + ]; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly orders: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'userId'; readonly direction: 1 }]; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly userId: { readonly bsonType: 'objectId' }; + readonly items: { + readonly bsonType: 'array'; + readonly items: { + readonly bsonType: 'object'; + readonly properties: { + readonly productId: { readonly bsonType: 'string' }; + readonly name: { readonly bsonType: 'string' }; + readonly brand: { readonly bsonType: 'string' }; + readonly amount: { readonly bsonType: 'int' }; + readonly price: { + readonly bsonType: 'object'; + readonly properties: { + readonly amount: { readonly bsonType: 'double' }; + readonly currency: { readonly bsonType: 'string' }; + }; + readonly additionalProperties: false; + readonly required: readonly ['amount', 'currency']; + }; + readonly image: { + readonly bsonType: 'object'; + readonly properties: { readonly url: { readonly bsonType: 'string' } }; + readonly additionalProperties: false; + readonly required: readonly ['url']; + }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + 'amount', + 'brand', + 'image', + 'name', + 'price', + 'productId', + ]; + }; + }; + readonly shippingAddress: { readonly bsonType: 'string' }; + readonly type: { readonly bsonType: 'string' }; + readonly statusHistory: { + readonly bsonType: 'array'; + readonly items: { + readonly bsonType: 'object'; + readonly properties: { + readonly status: { readonly bsonType: 'string' }; + readonly timestamp: { readonly bsonType: 'date' }; + }; + readonly additionalProperties: false; + readonly required: readonly ['status', 'timestamp']; + }; + }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + '_id', + 'items', + 'shippingAddress', + 'statusHistory', + 'type', + 'userId', + ]; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly products: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [ + { readonly field: 'name'; readonly direction: 'text' }, + { readonly field: 'description'; readonly direction: 'text' }, + ]; + readonly weights: { readonly name: 10; readonly description: 1 }; + }, + { + readonly kind: 'mongo-index'; + readonly keys: readonly [ + { readonly field: 'brand'; readonly direction: 1 }, + { readonly field: 'subCategory'; readonly direction: 1 }, + ]; + }, + { + readonly kind: 'mongo-index'; + readonly keys: readonly [ + { readonly field: 'code'; readonly direction: 'hashed' }, + ]; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly name: { readonly bsonType: 'string' }; + readonly brand: { readonly bsonType: 'string' }; + readonly code: { readonly bsonType: 'string' }; + readonly description: { readonly bsonType: 'string' }; + readonly primaryCategory: { readonly bsonType: 'string' }; + readonly subCategory: { readonly bsonType: 'string' }; + readonly articleType: { readonly bsonType: 'string' }; + readonly price: { + readonly bsonType: 'object'; + readonly properties: { + readonly amount: { readonly bsonType: 'double' }; + readonly currency: { readonly bsonType: 'string' }; + }; + readonly additionalProperties: false; + readonly required: readonly ['amount', 'currency']; + }; + readonly image: { + readonly bsonType: 'object'; + readonly properties: { readonly url: { readonly bsonType: 'string' } }; + readonly additionalProperties: false; + readonly required: readonly ['url']; + }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + '_id', + 'articleType', + 'brand', + 'code', + 'description', + 'image', + 'name', + 'price', + 'primaryCategory', + 'subCategory', + ]; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + readonly users: { + readonly kind: 'mongo-collection'; + readonly indexes: readonly [ + { + readonly kind: 'mongo-index'; + readonly keys: readonly [{ readonly field: 'email'; readonly direction: 1 }]; + readonly unique: true; + }, + ]; + readonly validator: { + readonly kind: 'mongo-validator'; + readonly jsonSchema: { + readonly bsonType: 'object'; + readonly properties: { + readonly _id: { readonly bsonType: 'objectId' }; + readonly name: { readonly bsonType: 'string' }; + readonly email: { readonly bsonType: 'string' }; + readonly address: { + readonly oneOf: readonly [ + { readonly bsonType: 'null' }, + { + readonly bsonType: 'object'; + readonly properties: { + readonly streetAndNumber: { readonly bsonType: 'string' }; + readonly city: { readonly bsonType: 'string' }; + readonly postalCode: { readonly bsonType: 'string' }; + readonly country: { readonly bsonType: 'string' }; + }; + readonly additionalProperties: false; + readonly required: readonly [ + 'city', + 'country', + 'postalCode', + 'streetAndNumber', + ]; + }, + ]; + }; + }; + readonly additionalProperties: false; + readonly required: readonly ['_id', 'email', 'name']; + }; + readonly validationLevel: 'strict'; + readonly validationAction: 'error'; + }; + }; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }>, + 'roots' | 'domain' +> & { + readonly target: 'mongo'; + readonly targetFamily: 'mongo'; + readonly roots: { + readonly products: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Product'; + }; + readonly users: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'User' }; + readonly carts: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Cart' }; + readonly orders: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Order' }; + readonly locations: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Location'; + }; + readonly invoices: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Invoice'; + }; + readonly events: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'Event' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly AddToCartEvent: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'events' }; + readonly base: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Event'; + }; + }; + readonly Cart: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly items: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'CartItem' }; + readonly many: true; + }; + }; + readonly relations: { + readonly user: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'User'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['_id']; + }; + }; + }; + readonly storage: { readonly collection: 'carts' }; + }; + readonly Event: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly sessionId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly type: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly timestamp: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'events' }; + readonly discriminator: { readonly field: 'type' }; + readonly variants: { + readonly ViewProductEvent: { readonly value: 'view-product' }; + readonly SearchEvent: { readonly value: 'search' }; + readonly AddToCartEvent: { readonly value: 'add-to-cart' }; + }; + }; + readonly Invoice: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly orderId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly items: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'InvoiceLineItem' }; + readonly many: true; + }; + readonly subtotal: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly tax: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly total: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly issuedAt: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' }; + }; + }; + readonly relations: { + readonly order: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Order'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['orderId']; + readonly targetFields: readonly ['_id']; + }; + }; + }; + readonly storage: { readonly collection: 'invoices' }; + }; + readonly Location: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly streetAndNumber: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly city: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly postalCode: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly country: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'locations' }; + }; + readonly Order: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly userId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly items: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'OrderLineItem' }; + readonly many: true; + }; + readonly shippingAddress: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly type: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly statusHistory: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'StatusEntry' }; + readonly many: true; + }; + }; + readonly relations: { + readonly user: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'User'; + }; + readonly cardinality: 'N:1'; + readonly on: { + readonly localFields: readonly ['userId']; + readonly targetFields: readonly ['_id']; + }; + }; + readonly invoices: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Invoice'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['_id']; + readonly targetFields: readonly ['orderId']; + }; + }; + }; + readonly storage: { readonly collection: 'orders' }; + }; + readonly Product: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly code: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly description: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly primaryCategory: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly subCategory: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly articleType: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly price: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' }; + }; + readonly image: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'products' }; + }; + readonly SearchEvent: { + readonly fields: { + readonly query: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'events' }; + readonly base: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Event'; + }; + }; + readonly User: { + readonly fields: { + readonly _id: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly address: { + readonly nullable: true; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Address' }; + }; + }; + readonly relations: { + readonly carts: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Cart'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['_id']; + readonly targetFields: readonly ['userId']; + }; + }; + readonly orders: { + readonly to: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Order'; + }; + readonly cardinality: '1:N'; + readonly on: { + readonly localFields: readonly ['_id']; + readonly targetFields: readonly ['userId']; + }; + }; + }; + readonly storage: { readonly collection: 'users' }; + }; + readonly ViewProductEvent: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly subCategory: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly exitMethod: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + readonly relations: Record; + readonly storage: { readonly collection: 'events' }; + readonly base: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'Event'; + }; + }; + }; + readonly valueObjects: { + readonly Price: { + readonly fields: { + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly currency: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly Image: { + readonly fields: { + readonly url: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly Address: { + readonly fields: { + readonly streetAndNumber: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly city: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly postalCode: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly country: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly CartItem: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly price: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' }; + }; + readonly image: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' }; + }; + }; + }; + readonly OrderLineItem: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly price: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' }; + }; + readonly image: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' }; + }; + }; + }; + readonly StatusEntry: { + readonly fields: { + readonly status: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly timestamp: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' }; + }; + }; + }; + readonly InvoiceLineItem: { + readonly fields: { + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly unitPrice: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly lineTotal: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: {}; + readonly extensionPacks: {}; + readonly meta: {}; + readonly valueObjects: { + readonly Price: { + readonly fields: { + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly currency: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly Image: { + readonly fields: { + readonly url: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly Address: { + readonly fields: { + readonly streetAndNumber: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly city: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly postalCode: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly country: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + }; + }; + readonly CartItem: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly price: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' }; + }; + readonly image: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' }; + }; + }; + }; + readonly OrderLineItem: { + readonly fields: { + readonly productId: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly brand: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly price: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' }; + }; + readonly image: { + readonly nullable: false; + readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' }; + }; + }; + }; + readonly StatusEntry: { + readonly fields: { + readonly status: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly timestamp: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' }; + }; + }; + }; + readonly InvoiceLineItem: { + readonly fields: { + readonly name: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' }; + }; + readonly amount: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' }; + }; + readonly unitPrice: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + readonly lineTotal: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' }; + }; + }; + }; + }; + readonly profileHash: ProfileHash; +}; + +export type Contract = MongoContractWithTypeMaps; diff --git a/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.json b/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.json new file mode 100644 index 0000000000..0f947620f0 --- /dev/null +++ b/packages/2-mongo-family/9-family/test/fixtures/migration-end-contract.json @@ -0,0 +1,1407 @@ +{ + "schemaVersion": "1", + "targetFamily": "mongo", + "target": "mongo", + "profileHash": "sha256:cca47cfb902adf4e15c2f277dd98af4aff64a3a2c010b49ace1c897de1cc4510", + "roots": { + "carts": { + "model": "Cart", + "namespace": "__unbound__" + }, + "events": { + "model": "Event", + "namespace": "__unbound__" + }, + "invoices": { + "model": "Invoice", + "namespace": "__unbound__" + }, + "locations": { + "model": "Location", + "namespace": "__unbound__" + }, + "orders": { + "model": "Order", + "namespace": "__unbound__" + }, + "products": { + "model": "Product", + "namespace": "__unbound__" + }, + "users": { + "model": "User", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "AddToCartEvent": { + "base": { + "model": "Event", + "namespace": "__unbound__" + }, + "fields": { + "brand": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "productId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "events" + } + }, + "Cart": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "items": { + "many": true, + "nullable": false, + "type": { + "kind": "valueObject", + "name": "CartItem" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + } + }, + "relations": { + "user": { + "cardinality": "N:1", + "on": { + "localFields": ["userId"], + "targetFields": ["_id"] + }, + "to": { + "model": "User", + "namespace": "__unbound__" + } + } + }, + "storage": { + "collection": "carts" + } + }, + "Event": { + "discriminator": { + "field": "type" + }, + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "sessionId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "timestamp": { + "nullable": false, + "type": { + "codecId": "mongo/date@1", + "kind": "scalar" + } + }, + "type": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "events" + }, + "variants": { + "AddToCartEvent": { + "value": "add-to-cart" + }, + "SearchEvent": { + "value": "search" + }, + "ViewProductEvent": { + "value": "view-product" + } + } + }, + "Invoice": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "issuedAt": { + "nullable": false, + "type": { + "codecId": "mongo/date@1", + "kind": "scalar" + } + }, + "items": { + "many": true, + "nullable": false, + "type": { + "kind": "valueObject", + "name": "InvoiceLineItem" + } + }, + "orderId": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "subtotal": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + }, + "tax": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + }, + "total": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + } + }, + "relations": { + "order": { + "cardinality": "N:1", + "on": { + "localFields": ["orderId"], + "targetFields": ["_id"] + }, + "to": { + "model": "Order", + "namespace": "__unbound__" + } + } + }, + "storage": { + "collection": "invoices" + } + }, + "Location": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "city": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "country": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "postalCode": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "streetAndNumber": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "locations" + } + }, + "Order": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "items": { + "many": true, + "nullable": false, + "type": { + "kind": "valueObject", + "name": "OrderLineItem" + } + }, + "shippingAddress": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "statusHistory": { + "many": true, + "nullable": false, + "type": { + "kind": "valueObject", + "name": "StatusEntry" + } + }, + "type": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "userId": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + } + }, + "relations": { + "invoices": { + "cardinality": "1:N", + "on": { + "localFields": ["_id"], + "targetFields": ["orderId"] + }, + "to": { + "model": "Invoice", + "namespace": "__unbound__" + } + }, + "user": { + "cardinality": "N:1", + "on": { + "localFields": ["userId"], + "targetFields": ["_id"] + }, + "to": { + "model": "User", + "namespace": "__unbound__" + } + } + }, + "storage": { + "collection": "orders" + } + }, + "Product": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "articleType": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "brand": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "code": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "description": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "image": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Image" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "price": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Price" + } + }, + "primaryCategory": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "subCategory": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "products" + } + }, + "SearchEvent": { + "base": { + "model": "Event", + "namespace": "__unbound__" + }, + "fields": { + "query": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "events" + } + }, + "User": { + "fields": { + "_id": { + "nullable": false, + "type": { + "codecId": "mongo/objectId@1", + "kind": "scalar" + } + }, + "address": { + "nullable": true, + "type": { + "kind": "valueObject", + "name": "Address" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": { + "carts": { + "cardinality": "1:N", + "on": { + "localFields": ["_id"], + "targetFields": ["userId"] + }, + "to": { + "model": "Cart", + "namespace": "__unbound__" + } + }, + "orders": { + "cardinality": "1:N", + "on": { + "localFields": ["_id"], + "targetFields": ["userId"] + }, + "to": { + "model": "Order", + "namespace": "__unbound__" + } + } + }, + "storage": { + "collection": "users" + } + }, + "ViewProductEvent": { + "base": { + "model": "Event", + "namespace": "__unbound__" + }, + "fields": { + "brand": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "exitMethod": { + "nullable": true, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "productId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "subCategory": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "collection": "events" + } + } + }, + "valueObjects": { + "Address": { + "fields": { + "city": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "country": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "postalCode": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "streetAndNumber": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + } + }, + "CartItem": { + "fields": { + "amount": { + "nullable": false, + "type": { + "codecId": "mongo/int32@1", + "kind": "scalar" + } + }, + "brand": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "image": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Image" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "price": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Price" + } + }, + "productId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + } + }, + "Image": { + "fields": { + "url": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + } + }, + "InvoiceLineItem": { + "fields": { + "amount": { + "nullable": false, + "type": { + "codecId": "mongo/int32@1", + "kind": "scalar" + } + }, + "lineTotal": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "unitPrice": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + } + } + }, + "OrderLineItem": { + "fields": { + "amount": { + "nullable": false, + "type": { + "codecId": "mongo/int32@1", + "kind": "scalar" + } + }, + "brand": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "image": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Image" + } + }, + "name": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "price": { + "nullable": false, + "type": { + "kind": "valueObject", + "name": "Price" + } + }, + "productId": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + } + }, + "Price": { + "fields": { + "amount": { + "nullable": false, + "type": { + "codecId": "mongo/double@1", + "kind": "scalar" + } + }, + "currency": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + } + } + }, + "StatusEntry": { + "fields": { + "status": { + "nullable": false, + "type": { + "codecId": "mongo/string@1", + "kind": "scalar" + } + }, + "timestamp": { + "nullable": false, + "type": { + "codecId": "mongo/date@1", + "kind": "scalar" + } + } + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "entries": { + "collection": { + "carts": { + "indexes": [ + { + "keys": [ + { + "direction": 1, + "field": "userId" + } + ], + "kind": "mongo-index", + "unique": true + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "items": { + "bsonType": "array", + "items": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "int" + }, + "brand": { + "bsonType": "string" + }, + "image": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "url": { + "bsonType": "string" + } + }, + "required": ["url"] + }, + "name": { + "bsonType": "string" + }, + "price": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "double" + }, + "currency": { + "bsonType": "string" + } + }, + "required": ["amount", "currency"] + }, + "productId": { + "bsonType": "string" + } + }, + "required": ["amount", "brand", "image", "name", "price", "productId"] + } + }, + "userId": { + "bsonType": "objectId" + } + }, + "required": ["_id", "items", "userId"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "events": { + "indexes": [ + { + "keys": [ + { + "direction": 1, + "field": "userId" + }, + { + "direction": -1, + "field": "timestamp" + } + ], + "kind": "mongo-index" + }, + { + "expireAfterSeconds": 7776000, + "keys": [ + { + "direction": 1, + "field": "timestamp" + } + ], + "kind": "mongo-index" + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "bsonType": "object", + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "_id": { + "bsonType": "objectId" + }, + "brand": { + "bsonType": "string" + }, + "exitMethod": { + "bsonType": ["null", "string"] + }, + "productId": { + "bsonType": "string" + }, + "sessionId": { + "bsonType": "string" + }, + "subCategory": { + "bsonType": "string" + }, + "timestamp": { + "bsonType": "date" + }, + "type": { + "enum": ["view-product"] + }, + "userId": { + "bsonType": "string" + } + }, + "required": ["brand", "productId", "subCategory", "type"] + }, + { + "additionalProperties": false, + "properties": { + "_id": { + "bsonType": "objectId" + }, + "query": { + "bsonType": "string" + }, + "sessionId": { + "bsonType": "string" + }, + "timestamp": { + "bsonType": "date" + }, + "type": { + "enum": ["search"] + }, + "userId": { + "bsonType": "string" + } + }, + "required": ["query", "type"] + }, + { + "additionalProperties": false, + "properties": { + "_id": { + "bsonType": "objectId" + }, + "brand": { + "bsonType": "string" + }, + "productId": { + "bsonType": "string" + }, + "sessionId": { + "bsonType": "string" + }, + "timestamp": { + "bsonType": "date" + }, + "type": { + "enum": ["add-to-cart"] + }, + "userId": { + "bsonType": "string" + } + }, + "required": ["brand", "productId", "type"] + } + ], + "properties": { + "_id": { + "bsonType": "objectId" + }, + "sessionId": { + "bsonType": "string" + }, + "timestamp": { + "bsonType": "date" + }, + "type": { + "bsonType": "string" + }, + "userId": { + "bsonType": "string" + } + }, + "required": ["_id", "sessionId", "timestamp", "type", "userId"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "invoices": { + "indexes": [ + { + "keys": [ + { + "direction": 1, + "field": "orderId" + } + ], + "kind": "mongo-index" + }, + { + "keys": [ + { + "direction": -1, + "field": "issuedAt" + } + ], + "kind": "mongo-index", + "sparse": true + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "issuedAt": { + "bsonType": "date" + }, + "items": { + "bsonType": "array", + "items": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "int" + }, + "lineTotal": { + "bsonType": "double" + }, + "name": { + "bsonType": "string" + }, + "unitPrice": { + "bsonType": "double" + } + }, + "required": ["amount", "lineTotal", "name", "unitPrice"] + } + }, + "orderId": { + "bsonType": "objectId" + }, + "subtotal": { + "bsonType": "double" + }, + "tax": { + "bsonType": "double" + }, + "total": { + "bsonType": "double" + } + }, + "required": ["_id", "issuedAt", "items", "orderId", "subtotal", "tax", "total"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "locations": { + "indexes": [ + { + "collation": { + "locale": "en", + "strength": 2 + }, + "keys": [ + { + "direction": 1, + "field": "city" + }, + { + "direction": 1, + "field": "country" + } + ], + "kind": "mongo-index" + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "city": { + "bsonType": "string" + }, + "country": { + "bsonType": "string" + }, + "name": { + "bsonType": "string" + }, + "postalCode": { + "bsonType": "string" + }, + "streetAndNumber": { + "bsonType": "string" + } + }, + "required": ["_id", "city", "country", "name", "postalCode", "streetAndNumber"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "orders": { + "indexes": [ + { + "keys": [ + { + "direction": 1, + "field": "userId" + } + ], + "kind": "mongo-index" + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "items": { + "bsonType": "array", + "items": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "int" + }, + "brand": { + "bsonType": "string" + }, + "image": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "url": { + "bsonType": "string" + } + }, + "required": ["url"] + }, + "name": { + "bsonType": "string" + }, + "price": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "double" + }, + "currency": { + "bsonType": "string" + } + }, + "required": ["amount", "currency"] + }, + "productId": { + "bsonType": "string" + } + }, + "required": ["amount", "brand", "image", "name", "price", "productId"] + } + }, + "shippingAddress": { + "bsonType": "string" + }, + "statusHistory": { + "bsonType": "array", + "items": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "status": { + "bsonType": "string" + }, + "timestamp": { + "bsonType": "date" + } + }, + "required": ["status", "timestamp"] + } + }, + "type": { + "bsonType": "string" + }, + "userId": { + "bsonType": "objectId" + } + }, + "required": ["_id", "items", "shippingAddress", "statusHistory", "type", "userId"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "products": { + "indexes": [ + { + "keys": [ + { + "direction": "text", + "field": "name" + }, + { + "direction": "text", + "field": "description" + } + ], + "kind": "mongo-index", + "weights": { + "description": 1, + "name": 10 + } + }, + { + "keys": [ + { + "direction": 1, + "field": "brand" + }, + { + "direction": 1, + "field": "subCategory" + } + ], + "kind": "mongo-index" + }, + { + "keys": [ + { + "direction": "hashed", + "field": "code" + } + ], + "kind": "mongo-index" + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "articleType": { + "bsonType": "string" + }, + "brand": { + "bsonType": "string" + }, + "code": { + "bsonType": "string" + }, + "description": { + "bsonType": "string" + }, + "image": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "url": { + "bsonType": "string" + } + }, + "required": ["url"] + }, + "name": { + "bsonType": "string" + }, + "price": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "amount": { + "bsonType": "double" + }, + "currency": { + "bsonType": "string" + } + }, + "required": ["amount", "currency"] + }, + "primaryCategory": { + "bsonType": "string" + }, + "subCategory": { + "bsonType": "string" + } + }, + "required": [ + "_id", + "articleType", + "brand", + "code", + "description", + "image", + "name", + "price", + "primaryCategory", + "subCategory" + ] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + }, + "users": { + "indexes": [ + { + "keys": [ + { + "direction": 1, + "field": "email" + } + ], + "kind": "mongo-index", + "unique": true + } + ], + "kind": "mongo-collection", + "validator": { + "jsonSchema": { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "address": { + "oneOf": [ + { + "bsonType": "null" + }, + { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "city": { + "bsonType": "string" + }, + "country": { + "bsonType": "string" + }, + "postalCode": { + "bsonType": "string" + }, + "streetAndNumber": { + "bsonType": "string" + } + }, + "required": ["city", "country", "postalCode", "streetAndNumber"] + } + ] + }, + "email": { + "bsonType": "string" + }, + "name": { + "bsonType": "string" + } + }, + "required": ["_id", "email", "name"] + }, + "kind": "mongo-validator", + "validationAction": "error", + "validationLevel": "strict" + } + } + } + }, + "id": "__unbound__", + "kind": "mongo-database" + } + }, + "storageHash": "sha256:8ee1e7ce30ed334572583d826d9c41388c46f7db82ae2352c3a3fccf1de7cbab" + }, + "capabilities": {}, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/packages/2-mongo-family/9-family/test/mongo-migration.test.ts b/packages/2-mongo-family/9-family/test/mongo-migration.test.ts new file mode 100644 index 0000000000..ad219f0b3c --- /dev/null +++ b/packages/2-mongo-family/9-family/test/mongo-migration.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { MongoMigration } from '../src/core/mongo-migration'; +import type { Contract } from './fixtures/migration-end-contract.d'; +import contractJson from './fixtures/migration-end-contract.json' with { type: 'json' }; + +const END_HASH = contractJson.storage.storageHash; + +class TestMigration extends MongoMigration { + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +class WithStartMigration extends MongoMigration { + override readonly startContractJson = contractJson; + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +describe('MongoMigration view getters', () => { + it('this.endContract is a typed MongoContractView built from endContractJson', () => { + const view = new TestMigration().endContract; + expect(view.collection.carts).toBeDefined(); + expect(view.collection.products).toBeDefined(); + // Substitutable for Contract: the full envelope is present. + expect(view.storage.storageHash).toBe(END_HASH); + }); + + it('this.endContract reads entity data through the view', () => { + const view = new TestMigration().endContract; + expect(view.collection.carts.validator).toBeDefined(); + }); + + it('this.startContract is null when no startContractJson is provided (baseline)', () => { + expect(new TestMigration().startContract).toBeNull(); + }); + + it('this.startContract is a typed view when startContractJson is provided', () => { + const view = new WithStartMigration().startContract; + expect(view).not.toBeNull(); + expect(view?.collection.carts).toBeDefined(); + }); + + it('endContract is memoized (same reference on repeat access)', () => { + const m = new TestMigration(); + expect(m.endContract).toBe(m.endContract); + }); + + it('startContract is memoized (same reference on repeat access)', () => { + const m = new WithStartMigration(); + expect(m.startContract).toBe(m.startContract); + }); + + it('describe() is derived from the contract JSON (from:null, to:end hash)', () => { + expect(new TestMigration().describe()).toEqual({ from: null, to: END_HASH }); + expect(new WithStartMigration().describe()).toEqual({ from: END_HASH, to: END_HASH }); + }); + + it('endContract throws when no endContractJson is provided', () => { + class NoContract extends MongoMigration { + override describe() { + return { from: null, to: 'sha256:x' }; + } + override get operations() { + return []; + } + } + expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + }); +}); diff --git a/packages/2-mongo-family/9-family/tsconfig.test.json b/packages/2-mongo-family/9-family/tsconfig.test.json index c6d45b4073..73d1e0b4f3 100644 --- a/packages/2-mongo-family/9-family/tsconfig.test.json +++ b/packages/2-mongo-family/9-family/tsconfig.test.json @@ -9,7 +9,8 @@ "include": [ "src/**/*.ts", "test/mongo-contract-view.test.ts", - "test/mongo-contract-view.test-d.ts" + "test/mongo-contract-view.test-d.ts", + "test/mongo-migration.test.ts" ], "exclude": ["dist"] } From dd05d50317858fd0ff6c86f729933a76dffb6962 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 29 Jun 2026 13:08:41 +0200 Subject: [PATCH 06/12] feat(tml-2892): SQLite and Postgres migration bases expose typed contract views Mirror D-A for the SQL family: thread `` 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.`). Co-Authored-By: Claude Opus 4.8 Signed-off-by: willbot Signed-off-by: Will Madden --- .../2-sql/9-family/src/core/sql-migration.ts | 13 +++- .../src/core/migrations/postgres-migration.ts | 45 ++++++++++- .../test/postgres-migration.test-d.ts | 43 +++++++++++ .../postgres/test/postgres-migration.test.ts | 74 ++++++++++++++++++ .../src/core/migrations/sqlite-migration.ts | 50 ++++++++++++- .../sqlite/test/sqlite-migration.test-d.ts | 40 ++++++++++ .../sqlite/test/sqlite-migration.test.ts | 75 +++++++++++++++++++ 7 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 packages/3-targets/3-targets/postgres/test/postgres-migration.test-d.ts create mode 100644 packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/sqlite-migration.test-d.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts diff --git a/packages/2-sql/9-family/src/core/sql-migration.ts b/packages/2-sql/9-family/src/core/sql-migration.ts index 4d681bed5e..f2b1186838 100644 --- a/packages/2-sql/9-family/src/core/sql-migration.ts +++ b/packages/2-sql/9-family/src/core/sql-migration.ts @@ -1,5 +1,7 @@ +import type { Contract } from '@prisma-next/contract/types'; import { deriveProvidedInvariants } from '@prisma-next/migration-tools/invariants'; import { Migration } from '@prisma-next/migration-tools/migration'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; import { isThenable } from '@prisma-next/utils/promise'; import type { SqlMigrationPlanOperation, SqlPlanTargetDetails } from './migrations/types'; @@ -14,13 +16,22 @@ import type { SqlMigrationPlanOperation, SqlPlanTargetDetails } from './migratio * fully concrete operation shape. Target-free code in SQL family / tooling * parameterises over `TDetails` (and usually `TTargetId = string`). * + * The `Start` / `End` contract generics are forwarded to the framework + * `Migration` base so the derived `describe()` and the per-target view getters + * (`PostgresMigration` / `SqliteMigration`) carry each migration's precise + * contract types. The view getters themselves live on the per-target bases + * because the SQL ContractView shapes differ per target (SQLite unwraps its + * sole namespace to the root; Postgres is schema-qualified). + * * Keeps target-free contract/runtime features in the family layer while * letting adapters own target shape. */ export abstract class SqlMigration< TDetails extends SqlPlanTargetDetails, TTargetId extends string = string, -> extends Migration, 'sql', TTargetId> { + Start extends Contract = Contract, + End extends Contract = Contract, +> extends Migration, 'sql', TTargetId, Start, End> { /** * Sorted, deduplicated invariant ids declared by this migration's * data-transform ops. Derived from `this.operations` so the field remains diff --git a/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts b/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts index 30f85a3f02..527f426b86 100644 --- a/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts +++ b/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts @@ -6,6 +6,7 @@ import type { ControlStack } from '@prisma-next/framework-components/control'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast'; import { errorPostgresMigrationStackMissing } from '../errors'; +import { PostgresContractView } from '../postgres-contract-view'; import { AddCheckConstraintCall, AddColumnCall, @@ -60,10 +61,10 @@ import type { PostgresPlanTargetDetails } from './planner-target-details'; * is an antipattern in a migration. (The unbound/unspecified namespace concept * remains for SQLite, which has no schemas, and for Mongo's connection `db`.) */ -export abstract class PostgresMigration extends SqlMigration< - PostgresPlanTargetDetails, - 'postgres' -> { +export abstract class PostgresMigration< + Start extends Contract = Contract, + End extends Contract = Contract, +> extends SqlMigration { readonly targetId = 'postgres' as const; /** @@ -74,6 +75,9 @@ export abstract class PostgresMigration extends SqlMigration< */ protected readonly controlAdapter: SqlControlAdapter<'postgres'> | undefined; + #endContract?: PostgresContractView; + #startContract?: PostgresContractView | null; + constructor(stack?: ControlStack<'sql', 'postgres'>) { super(stack); // The descriptor `create()` is typed as the wider `ControlAdapterInstance`; @@ -84,6 +88,39 @@ export abstract class PostgresMigration extends SqlMigration< : undefined; } + /** + * The typed, schema-qualified Postgres view over this migration's end-state + * contract — `this.endContract.namespace..table.`, etc. Lazily + * built from `endContractJson` and memoized. Throws if no `endContractJson` + * was provided. + */ + get endContract(): PostgresContractView { + if (this.#endContract === undefined) { + if (this.endContractJson === undefined) { + throw new Error( + 'PostgresMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', + ); + } + this.#endContract = PostgresContractView.fromJson(this.endContractJson); + } + return this.#endContract; + } + + /** + * The typed Postgres view over this migration's start-state contract, or + * `null` for a baseline migration (no `startContractJson`). Lazily built and + * memoized. + */ + get startContract(): PostgresContractView | null { + if (this.#startContract === undefined) { + this.#startContract = + this.startContractJson === undefined + ? null + : PostgresContractView.fromJson(this.startContractJson); + } + return this.#startContract; + } + /** * Instance-method wrapper around the free `dataTransform` factory that * supplies the stored control adapter. Authors call this from inside diff --git a/packages/3-targets/3-targets/postgres/test/postgres-migration.test-d.ts b/packages/3-targets/3-targets/postgres/test/postgres-migration.test-d.ts new file mode 100644 index 0000000000..4e78da0325 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/postgres-migration.test-d.ts @@ -0,0 +1,43 @@ +import { expectTypeOf, test } from 'vitest'; +import { PostgresMigration } from '../src/core/migrations/postgres-migration'; +import type { PostgresContractView } from '../src/core/postgres-contract-view'; +import type { Contract } from './fixtures/namespaced-contract.d'; + +/** + * Emit-then-consume type tests: the migration's `endContract` getter resolves + * to the precisely-typed, schema-qualified `PostgresContractView` over the + * real emitted multi-schema fixture. + */ + +class TypedMigration extends PostgresMigration { + override readonly endContractJson = {} as Contract; + override get operations() { + return []; + } +} + +type EndView = TypedMigration['endContract']; +type StartView = TypedMigration['startContract']; + +test('endContract is a PostgresContractView', () => { + expectTypeOf().toEqualTypeOf>(); +}); + +test('startContract is PostgresContractView | null', () => { + expectTypeOf().toEqualTypeOf | null>(); +}); + +test('endContract.namespace..table. resolves per-schema leaves', () => { + expectTypeOf< + EndView['namespace']['public']['table']['users']['columns']['email']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); + expectTypeOf< + EndView['namespace']['auth']['table']['users']['columns']['token']['codecId'] + >().toEqualTypeOf<'pg/text@1'>(); +}); + +test('cross-schema column access on the migration view is a compile error', () => { + const view = null as unknown as EndView; + // @ts-expect-error public.users has no `token` column (that is auth.users) + view.namespace.public.table.users.columns.token; +}); diff --git a/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts new file mode 100644 index 0000000000..1616c12917 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import { PostgresMigration } from '../src/core/migrations/postgres-migration'; +import type { Contract } from './fixtures/namespaced-contract.d'; +import contractJson from './fixtures/namespaced-contract.json' with { type: 'json' }; + +const END_HASH = contractJson.storage.storageHash; + +class TestMigration extends PostgresMigration { + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +class WithStartMigration extends PostgresMigration { + override readonly startContractJson = contractJson; + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +describe('PostgresMigration view getters', () => { + it('this.endContract is a typed schema-qualified PostgresContractView', () => { + const view = new TestMigration().endContract; + expect(view.namespace.public.table.users).toBeDefined(); + expect(view.namespace.auth.table.users).toBeDefined(); + // Substitutable for Contract: the full envelope is present. + expect(view.storage.storageHash).toBe(END_HASH); + }); + + it('this.endContract reads per-schema table data through the view', () => { + const view = new TestMigration().endContract; + expect(Object.keys(view.namespace.public.table.users.columns).sort()).toEqual(['email', 'id']); + expect(Object.keys(view.namespace.auth.table.users.columns).sort()).toEqual(['id', 'token']); + }); + + it('this.startContract is null when no startContractJson is provided (baseline)', () => { + expect(new TestMigration().startContract).toBeNull(); + }); + + it('this.startContract is a typed view when startContractJson is provided', () => { + const view = new WithStartMigration().startContract; + expect(view).not.toBeNull(); + expect(view?.namespace.public.table.users).toBeDefined(); + }); + + it('endContract is memoized (same reference on repeat access)', () => { + const m = new TestMigration(); + expect(m.endContract).toBe(m.endContract); + }); + + it('startContract is memoized (same reference on repeat access)', () => { + const m = new WithStartMigration(); + expect(m.startContract).toBe(m.startContract); + }); + + it('describe() is derived from the contract JSON (from:null, to:end hash)', () => { + expect(new TestMigration().describe()).toEqual({ from: null, to: END_HASH }); + expect(new WithStartMigration().describe()).toEqual({ from: END_HASH, to: END_HASH }); + }); + + it('endContract throws when no endContractJson is provided', () => { + class NoContract extends PostgresMigration { + override describe() { + return { from: null, to: 'sha256:x' }; + } + override get operations() { + return []; + } + } + expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + }); +}); diff --git a/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts b/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts index 6e5d6f9f1d..04cd8c7f27 100644 --- a/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts +++ b/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts @@ -1,3 +1,4 @@ +import type { Contract } from '@prisma-next/contract/types'; import type { MigrationOperationClass, SqlMigrationPlanOperation, @@ -5,9 +6,11 @@ import type { import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter'; import { Migration as SqlMigration } from '@prisma-next/family-sql/migration'; import type { ControlStack } from '@prisma-next/framework-components/control'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast'; import { blindCast } from '@prisma-next/utils/casts'; import { errorSqliteMigrationStackMissing } from '../errors'; +import { SqliteContractView } from '../sqlite-contract-view'; import { AddColumnCall, CreateIndexCall, @@ -34,8 +37,18 @@ type Op = SqlMigrationPlanOperation; * forward to the corresponding `*Call` with that stored adapter, so user * migrations can write `this.createTable({...})` without threading the adapter * through every call. + * + * Binds the framework base's `Start` / `End` contract generics so a subclass + * that assigns its `start-contract.json` / `end-contract.json` imports gets + * fully-typed view accessors: `this.endContract` is a `SqliteContractView` + * (sole namespace unwrapped to the root — `this.endContract.table.`), + * built lazily from the JSON fields and memoized. Mirrors `MongoMigration`'s + * view getters; the framework base derives `describe()` from the same JSON. */ -export abstract class SqliteMigration extends SqlMigration { +export abstract class SqliteMigration< + Start extends Contract = Contract, + End extends Contract = Contract, +> extends SqlMigration { readonly targetId = 'sqlite' as const; /** @@ -46,6 +59,9 @@ export abstract class SqliteMigration extends SqlMigration | undefined; + #endContract?: SqliteContractView; + #startContract?: SqliteContractView | null; + constructor(stack?: ControlStack<'sql', 'sqlite'>) { super(stack); this.controlAdapter = stack?.adapter @@ -56,6 +72,38 @@ export abstract class SqliteMigration extends SqlMigration` etc. + * Lazily built from `endContractJson` and memoized. Throws if no + * `endContractJson` was provided. + */ + get endContract(): SqliteContractView { + if (this.#endContract === undefined) { + if (this.endContractJson === undefined) { + throw new Error( + 'SqliteMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', + ); + } + this.#endContract = SqliteContractView.fromJson(this.endContractJson); + } + return this.#endContract; + } + + /** + * The typed SQLite view over this migration's start-state contract, or `null` + * for a baseline migration (no `startContractJson`). Lazily built and memoized. + */ + get startContract(): SqliteContractView | null { + if (this.#startContract === undefined) { + this.#startContract = + this.startContractJson === undefined + ? null + : SqliteContractView.fromJson(this.startContractJson); + } + return this.#startContract; + } + protected createTable(options: { readonly table: string; readonly ifNotExists?: boolean; diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test-d.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test-d.ts new file mode 100644 index 0000000000..e38186f07a --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test-d.ts @@ -0,0 +1,40 @@ +import { expectTypeOf, test } from 'vitest'; +import { SqliteMigration } from '../src/core/migrations/sqlite-migration'; +import type { SqliteContractView } from '../src/core/sqlite-contract-view'; +import type { Contract } from './fixtures/sqlite-contract.d'; + +/** + * Emit-then-consume type tests: the migration's `endContract` getter resolves + * to the precisely-typed `SqliteContractView` over the real emitted SQLite + * contract fixture. + */ + +class TypedMigration extends SqliteMigration { + override readonly endContractJson = {} as Contract; + override get operations() { + return []; + } +} + +type EndView = TypedMigration['endContract']; +type StartView = TypedMigration['startContract']; + +test('endContract is a SqliteContractView', () => { + expectTypeOf().toEqualTypeOf>(); +}); + +test('startContract is SqliteContractView | null', () => { + expectTypeOf().toEqualTypeOf | null>(); +}); + +test('endContract.table. resolves to the concrete emitted table leaf', () => { + expectTypeOf< + EndView['table']['users']['columns']['id']['codecId'] + >().toEqualTypeOf<'sqlite/integer@1'>(); +}); + +test('a non-existent table name on the view is a compile error', () => { + const view = null as unknown as EndView; + // @ts-expect-error 'nonexistent' is not an emitted table + view.table.nonexistent; +}); diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts new file mode 100644 index 0000000000..b3966e669d --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { SqliteMigration } from '../src/core/migrations/sqlite-migration'; +import type { Contract } from './fixtures/sqlite-contract.d'; +import contractJson from './fixtures/sqlite-contract.json' with { type: 'json' }; + +const END_HASH = contractJson.storage.storageHash; + +class TestMigration extends SqliteMigration { + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +class WithStartMigration extends SqliteMigration { + override readonly startContractJson = contractJson; + override readonly endContractJson = contractJson; + override get operations() { + return []; + } +} + +describe('SqliteMigration view getters', () => { + it('this.endContract is a typed SqliteContractView built from endContractJson', () => { + const view = new TestMigration().endContract; + expect(view.table.users).toBeDefined(); + expect(view.table.posts).toBeDefined(); + // Substitutable for Contract: the full envelope is present. + expect(view.storage.storageHash).toBe(END_HASH); + }); + + it('this.endContract reads table entity data through the view', () => { + const view = new TestMigration().endContract; + expect(view.table.users.columns).toBeDefined(); + // SQLite has no value sets — the slot is an empty map. + expect(view.valueSet).toEqual({}); + }); + + it('this.startContract is null when no startContractJson is provided (baseline)', () => { + expect(new TestMigration().startContract).toBeNull(); + }); + + it('this.startContract is a typed view when startContractJson is provided', () => { + const view = new WithStartMigration().startContract; + expect(view).not.toBeNull(); + expect(view?.table.users).toBeDefined(); + }); + + it('endContract is memoized (same reference on repeat access)', () => { + const m = new TestMigration(); + expect(m.endContract).toBe(m.endContract); + }); + + it('startContract is memoized (same reference on repeat access)', () => { + const m = new WithStartMigration(); + expect(m.startContract).toBe(m.startContract); + }); + + it('describe() is derived from the contract JSON (from:null, to:end hash)', () => { + expect(new TestMigration().describe()).toEqual({ from: null, to: END_HASH }); + expect(new WithStartMigration().describe()).toEqual({ from: END_HASH, to: END_HASH }); + }); + + it('endContract throws when no endContractJson is provided', () => { + class NoContract extends SqliteMigration { + override describe() { + return { from: null, to: 'sha256:x' }; + } + override get operations() { + return []; + } + } + expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + }); +}); From 2327d461f0d2d7267caa5c083301cf4f8067da20 Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 29 Jun 2026 18:46:52 +0200 Subject: [PATCH 07/12] feat(tml-2892): migration generators emit the contract-JSON shape, drop describe() The three `render-typescript` generators now emit migrations that import their start/end contract JSON + types and extend `Migration` (baseline `Migration`), 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../src/core/render-typescript.ts | 57 +++++++++++---- .../1-mongo-target/test/migration-e2e.test.ts | 42 +++++++++-- .../1-mongo-target/test/mongo-planner.test.ts | 10 ++- .../test/planner-produced-migration.test.ts | 27 ++++--- .../test/render-typescript.test.ts | 64 ++++++++++------- .../src/core/migrations/render-typescript.ts | 57 +++++++++++---- .../test/migrations/render-typescript.test.ts | 70 +++++++++++++++++++ .../src/core/migrations/render-typescript.ts | 57 +++++++++++---- .../planner.authoring-surface.test.ts | 25 ++++--- .../test/migrations/render-typescript.test.ts | 70 +++++++++++++++++++ .../op-factory-call.rendering.test.ts | 13 ++-- .../planner.authoring-surface.test.ts | 20 ++++-- .../render-typescript.roundtrip.test.ts | 33 +++++++++ .../render-typescript.roundtrip.test.ts | 31 ++++++++ 14 files changed, 468 insertions(+), 108 deletions(-) create mode 100644 packages/3-targets/3-targets/postgres/test/migrations/render-typescript.test.ts create mode 100644 packages/3-targets/3-targets/sqlite/test/migrations/render-typescript.test.ts diff --git a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts index 13885ab34f..20235d8830 100644 --- a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts +++ b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts @@ -49,15 +49,19 @@ export function renderCallsToTypeScript( calls: ReadonlyArray, meta: RenderMigrationMeta, ): string { - const imports = buildImports(calls); + const imports = buildImports(calls, meta); const operationsBody = calls.map((c) => c.renderTypeScript()).join(',\n'); + const hasStart = meta.from !== null; + const startField = hasStart ? [' override readonly startContractJson = startContract;'] : []; return [ shebangLineFor(detectScaffoldRuntime()), imports, '', - 'class M extends Migration {', - buildDescribeMethod(meta), + `class M extends Migration<${hasStart ? 'Start' : 'never'}, End> {`, + ...startField, + ' override readonly endContractJson = endContract;', + '', ' override get operations() {', ' return [', indent(operationsBody, 6), @@ -71,8 +75,8 @@ export function renderCallsToTypeScript( ].join('\n'); } -function buildImports(calls: ReadonlyArray): string { - const requirements: ImportRequirement[] = [...BASE_IMPORTS]; +function buildImports(calls: ReadonlyArray, meta: RenderMigrationMeta): string { + const requirements: ImportRequirement[] = [...BASE_IMPORTS, ...contractImports(meta)]; for (const call of calls) { for (const req of call.importRequirements()) { requirements.push(req); @@ -81,16 +85,39 @@ function buildImports(calls: ReadonlyArray): string { return renderImports(requirements); } -function buildDescribeMethod(meta: RenderMigrationMeta): string { - const lines: string[] = []; - lines.push(' override describe() {'); - lines.push(' return {'); - lines.push(` from: ${JSON.stringify(meta.from)},`); - lines.push(` to: ${JSON.stringify(meta.to)},`); - lines.push(' };'); - lines.push(' }'); - lines.push(''); - return lines.join('\n'); +/** + * The committed contract-JSON imports the scaffold reads its from/to identity + * from. `end-contract.json` is always present; `start-contract.json` is added + * only for a non-baseline migration (`meta.from !== null`). The matching + * `Contract` type imports (aliased `Start`/`End`) feed the + * `Migration` generics. Baseline emits `Migration` with + * no start imports — `never` is the honest "no prior contract" Start. + */ +function contractImports(meta: RenderMigrationMeta): readonly ImportRequirement[] { + const reqs: ImportRequirement[] = [ + { + moduleSpecifier: './end-contract.json', + symbol: 'endContract', + kind: 'default', + attributes: { type: 'json' }, + }, + { moduleSpecifier: './end-contract', symbol: 'Contract', alias: 'End', typeOnly: true }, + ]; + if (meta.from !== null) { + reqs.push({ + moduleSpecifier: './start-contract.json', + symbol: 'startContract', + kind: 'default', + attributes: { type: 'json' }, + }); + reqs.push({ + moduleSpecifier: './start-contract', + symbol: 'Contract', + alias: 'Start', + typeOnly: true, + }); + } + return reqs; } function indent(text: string, spaces: number): string { diff --git a/packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts b/packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts index 8d55ad68ec..732bdb25ff 100644 --- a/packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts +++ b/packages/3-mongo-target/1-mongo-target/test/migration-e2e.test.ts @@ -92,6 +92,33 @@ describe('migration file E2E', () => { } } + /** + * The rendered (new-shape) scaffold imports its from/to identity from + * committed contract JSON. Write minimal `{start,end}-contract.json` (carrying + * `storage.storageHash`, which the base's derived describe() reads) and the + * matching `{start,end}-contract.ts` type modules so the executed migration + * resolves its imports; the hashes match `meta` so describe() is consistent. + */ + async function writeContractFixtures(meta: { + readonly from: string | null; + readonly to: string; + }): Promise { + const contractType = + 'export type Contract = { readonly storage: { readonly storageHash: string } };\n'; + await writeFile( + join(tmpDir, 'end-contract.json'), + JSON.stringify({ storage: { storageHash: meta.to } }, null, 2), + ); + await writeFile(join(tmpDir, 'end-contract.ts'), contractType); + if (meta.from !== null) { + await writeFile( + join(tmpDir, 'start-contract.json'), + JSON.stringify({ storage: { storageHash: meta.from } }, null, 2), + ); + await writeFile(join(tmpDir, 'start-contract.ts'), contractType); + } + } + describe('factory-based migration', () => { const factoryMigration = [ `import { MigrationCLI } from '${cliMigrationCliExport}';`, @@ -215,6 +242,7 @@ describe('migration file E2E', () => { .replace("'@prisma-next/target-mongo/migration'", `'${factoryExport}'`) .replace("'@prisma-next/cli/migration-cli'", `'${cliMigrationCliExport}'`); await writeFile(join(tmpDir, 'migration.ts'), resolvedSource); + await writeContractFixtures(defaultMeta); const result = await runFile('migration.ts'); expect(result.exitCode).toBe(0); @@ -258,6 +286,7 @@ describe('migration file E2E', () => { .replace("'@prisma-next/target-mongo/migration'", `'${factoryExport}'`) .replace("'@prisma-next/cli/migration-cli'", `'${cliMigrationCliExport}'`); await writeFile(join(tmpDir, 'migration.ts'), resolvedSource); + await writeContractFixtures(defaultMeta); const result = await runFile('migration.ts'); expect(result.exitCode).toBe(0); @@ -277,15 +306,14 @@ describe('migration file E2E', () => { const { DropCollectionCall } = await import('../src/core/op-factory-call'); const calls = [new DropCollectionCall('legacy')]; - const tsSource = renderCallsToTypeScript(calls, { - from: 'sha256:aaa', - to: 'sha256:bbb', - }); + const meta = { from: 'sha256:aaa', to: 'sha256:bbb' } as const; + const tsSource = renderCallsToTypeScript(calls, meta); const resolvedSource = tsSource .replace("'@prisma-next/family-mongo/migration'", `'${migrationExport}'`) .replace("'@prisma-next/target-mongo/migration'", `'${factoryExport}'`) .replace("'@prisma-next/cli/migration-cli'", `'${cliMigrationCliExport}'`); await writeFile(join(tmpDir, 'migration.ts'), resolvedSource); + await writeContractFixtures(meta); const result = await runFile('migration.ts'); expect(result.exitCode).toBe(0); @@ -454,14 +482,16 @@ describe('migration file E2E', () => { new CreateIndexCall('users', [{ field: 'email', direction: 1 as const }], { unique: true }), ]; - const migrationSource = renderCallsToTypeScript(calls, { + const directRunMeta = { from: 'sha256:0000000000000000000000000000000000000000000000000000000000000000', to: 'sha256:1111111111111111111111111111111111111111111111111111111111111111', - }) + } as const; + const migrationSource = renderCallsToTypeScript(calls, directRunMeta) .replace("'@prisma-next/family-mongo/migration'", `'${familyMongoDistMigration}'`) .replace("'@prisma-next/target-mongo/migration'", `'${targetMongoDistMigration}'`) .replace("'@prisma-next/cli/migration-cli'", `'${cliMigrationCliDist}'`); await writeMigrationTs(tmpDir, migrationSource); + await writeContractFixtures(directRunMeta); const migrationPath = join(tmpDir, 'migration.ts'); const content = await readFile(migrationPath, 'utf-8'); diff --git a/packages/3-mongo-target/1-mongo-target/test/mongo-planner.test.ts b/packages/3-mongo-target/1-mongo-target/test/mongo-planner.test.ts index b0306061b7..1a7e832c95 100644 --- a/packages/3-mongo-target/1-mongo-target/test/mongo-planner.test.ts +++ b/packages/3-mongo-target/1-mongo-target/test/mongo-planner.test.ts @@ -1658,10 +1658,14 @@ describe('MongoMigrationPlanner', () => { expect(source).toContain("import { Migration } from '@prisma-next/family-mongo/migration';"); expect(source).toContain("import { MigrationCLI } from '@prisma-next/cli/migration-cli';"); - expect(source).toContain('class M extends Migration'); + expect(source).toContain('class M extends Migration'); expect(source).toContain('MigrationCLI.run(import.meta.url, M);'); - expect(source).toContain('sha256:00'); - expect(source).toContain('sha256:01'); + // New shape: from/to are derived from the imported contract JSON, not + // embedded as literals or a describe() block. + expect(source).toContain('override readonly endContractJson = endContract;'); + expect(source).not.toContain('describe()'); + expect(source).not.toContain('sha256:00'); + expect(source).not.toContain('sha256:01'); }); it('produces a plan whose origin reflects the supplied fromHash', () => { diff --git a/packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts b/packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts index b75ce96003..b4ac704383 100644 --- a/packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts +++ b/packages/3-mongo-target/1-mongo-target/test/planner-produced-migration.test.ts @@ -55,17 +55,23 @@ describe('PlannerProducedMongoMigration', () => { expect(migration.operations).toEqual([]); }); - it('renders authoring TypeScript that wires up MigrationCLI.run and embeds describe() metadata', () => { + it('renders authoring TypeScript that wires up MigrationCLI.run and derives describe() from contract JSON', () => { const calls = [new CreateIndexCall('users', [{ field: 'email', direction: 1 }])]; const migration = new PlannerProducedMongoMigration(calls, META); const source = migration.renderTypeScript(); - expect(source).toContain('class M extends Migration'); + // New shape: base derives describe() from the imported contract JSON, so the + // scaffold carries `Migration` + the JSON/field imports and emits + // no describe()/hash literals. + expect(source).toContain('class M extends Migration'); + expect(source).toContain('override readonly startContractJson = startContract;'); + expect(source).toContain('override readonly endContractJson = endContract;'); expect(source).toContain('override get operations()'); expect(source).toContain('createIndex'); - expect(source).toContain(META.from); - expect(source).toContain(META.to); + expect(source).not.toContain('describe()'); + expect(source).not.toContain(META.from); + expect(source).not.toContain(META.to); expect(source).toContain("import { MigrationCLI } from '@prisma-next/cli/migration-cli';"); expect(source).toContain('MigrationCLI.run(import.meta.url, M);'); }); @@ -75,21 +81,20 @@ describe('PlannerProducedMongoMigration', () => { const source = migration.renderTypeScript(); - expect(source).toContain('class M extends Migration'); + expect(source).toContain('class M extends Migration'); + expect(source).toContain('override readonly endContractJson = endContract;'); expect(source).toContain('override get operations()'); - expect(source).toContain(META.from); - expect(source).toContain(META.to); + expect(source).not.toContain('describe()'); }); - it('renderTypeScript does not include labels in the describe block', () => { + it('renderTypeScript emits no describe() block (so no labels leak through it)', () => { const calls = [new CreateIndexCall('users', [{ field: 'email', direction: 1 }])]; const migration = new PlannerProducedMongoMigration(calls, META); const source = migration.renderTypeScript(); - expect(source).toContain('class M extends Migration'); - expect(source).toContain(META.from); - expect(source).toContain(META.to); + expect(source).toContain('class M extends Migration'); + expect(source).not.toContain('describe()'); expect(source).not.toContain('labels:'); }); }); diff --git a/packages/3-mongo-target/1-mongo-target/test/render-typescript.test.ts b/packages/3-mongo-target/1-mongo-target/test/render-typescript.test.ts index f6fd28b60f..6e90c41e02 100644 --- a/packages/3-mongo-target/1-mongo-target/test/render-typescript.test.ts +++ b/packages/3-mongo-target/1-mongo-target/test/render-typescript.test.ts @@ -30,12 +30,40 @@ describe('renderCallsToTypeScript', () => { expect(output).toContain("import { Migration } from '@prisma-next/family-mongo/migration';"); expect(output).toContain("import { MigrationCLI } from '@prisma-next/cli/migration-cli';"); expect(output).toContain("import { createIndex } from '@prisma-next/target-mongo/migration';"); - expect(output).toContain('class M extends Migration'); expect(output).toContain('override get operations()'); expect(output).toContain('export default M;'); expect(output).toContain('MigrationCLI.run(import.meta.url, M);'); }); + it('emits the contract-JSON imports + fields and the Migration header (with-start)', () => { + const calls = [new CreateIndexCall('users', [{ field: 'email', direction: 1 }])]; + + const output = renderTypeScript(calls, { from: 'sha256:aaa', to: 'sha256:bbb' }); + + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain( + 'import startContract from \'./start-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + expect(output).toContain("import type { Contract as Start } from './start-contract';"); + expect(output).toContain('class M extends Migration {'); + expect(output).toContain('override readonly startContractJson = startContract;'); + expect(output).toContain('override readonly endContractJson = endContract;'); + }); + + it('does NOT emit a describe() method (the base derives it from the contract JSON)', () => { + const calls = [new DropCollectionCall('users')]; + + const output = renderTypeScript(calls, { from: 'sha256:aaa', to: 'sha256:bbb' }); + + expect(output).not.toContain('describe()'); + // The hash values are no longer literal-embedded — they come from the JSON. + expect(output).not.toContain('sha256:aaa'); + expect(output).not.toContain('sha256:bbb'); + }); + it('prepends a node shebang as the first line under the default test env', () => { const calls = [new CreateIndexCall('users', [{ field: 'email', direction: 1 }])]; @@ -60,27 +88,6 @@ describe('renderCallsToTypeScript', () => { expect(output).not.toContain('collMod'); }); - it('includes describe() method when meta is provided', () => { - const calls = [new DropCollectionCall('users')]; - - const output = renderTypeScript(calls, { - from: 'sha256:abc', - to: 'sha256:def', - }); - - expect(output).toContain('override describe()'); - expect(output).toContain('"sha256:abc"'); - expect(output).toContain('"sha256:def"'); - }); - - it('always renders describe() since migration.json is required', () => { - const calls = [new DropCollectionCall('users')]; - - const output = renderTypeScript(calls); - - expect(output).toContain('override describe()'); - }); - it('renders createIndex with options', () => { const calls = [ new CreateIndexCall('users', [{ field: 'email', direction: 1 }], { @@ -206,7 +213,7 @@ describe('renderCallsToTypeScript', () => { expect(importLine).toContain('createIndex'); }); - it('renders `from: null` for baseline migrations', () => { + it('renders the baseline shape for from: null (no start imports, Migration)', () => { const calls = [new DropCollectionCall('users')]; const output = renderTypeScript(calls, { @@ -214,7 +221,16 @@ describe('renderCallsToTypeScript', () => { to: 'sha256:def', }); - expect(output).toContain('from: null'); + expect(output).toContain('class M extends Migration {'); + expect(output).toContain('override readonly endContractJson = endContract;'); + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + // No start contract for a baseline. + expect(output).not.toContain('start-contract'); + expect(output).not.toContain('startContractJson'); + expect(output).not.toContain('describe()'); }); it('handles empty calls array', () => { diff --git a/packages/3-targets/3-targets/postgres/src/core/migrations/render-typescript.ts b/packages/3-targets/3-targets/postgres/src/core/migrations/render-typescript.ts index 2b5c6ca690..0a4b892eac 100644 --- a/packages/3-targets/3-targets/postgres/src/core/migrations/render-typescript.ts +++ b/packages/3-targets/3-targets/postgres/src/core/migrations/render-typescript.ts @@ -45,15 +45,19 @@ export function renderCallsToTypeScript( calls: ReadonlyArray, meta: RenderMigrationMeta, ): string { - const imports = buildImports(calls); + const imports = buildImports(calls, meta); const operationsBody = calls.map((c) => renderCall(c)).join(',\n'); + const hasStart = meta.from !== null; + const startField = hasStart ? [' override readonly startContractJson = startContract;'] : []; return [ shebangLineFor(detectScaffoldRuntime()), imports, '', - 'export default class M extends Migration {', - buildDescribeMethod(meta), + `export default class M extends Migration<${hasStart ? 'Start' : 'never'}, End> {`, + ...startField, + ' override readonly endContractJson = endContract;', + '', ' override get operations() {', ' return [', indent(operationsBody, 6), @@ -70,8 +74,8 @@ function renderCall(call: OpFactoryCall): string { return call.renderTypeScript(); } -function buildImports(calls: ReadonlyArray): string { - const requirements: ImportRequirement[] = [...BASE_IMPORTS]; +function buildImports(calls: ReadonlyArray, meta: RenderMigrationMeta): string { + const requirements: ImportRequirement[] = [...BASE_IMPORTS, ...contractImports(meta)]; for (const call of calls) { for (const req of call.importRequirements()) { requirements.push(req); @@ -80,16 +84,39 @@ function buildImports(calls: ReadonlyArray): string { return renderImports(requirements); } -function buildDescribeMethod(meta: RenderMigrationMeta): string { - const lines: string[] = []; - lines.push(' override describe() {'); - lines.push(' return {'); - lines.push(` from: ${JSON.stringify(meta.from)},`); - lines.push(` to: ${JSON.stringify(meta.to)},`); - lines.push(' };'); - lines.push(' }'); - lines.push(''); - return lines.join('\n'); +/** + * The committed contract-JSON imports the scaffold reads its from/to identity + * from. `end-contract.json` is always present; `start-contract.json` is added + * only for a non-baseline migration (`meta.from !== null`). The matching + * `Contract` type imports (aliased `Start`/`End`) feed the + * `Migration` generics. Baseline emits `Migration` with + * no start imports — `never` is the honest "no prior contract" Start. + */ +function contractImports(meta: RenderMigrationMeta): readonly ImportRequirement[] { + const reqs: ImportRequirement[] = [ + { + moduleSpecifier: './end-contract.json', + symbol: 'endContract', + kind: 'default', + attributes: { type: 'json' }, + }, + { moduleSpecifier: './end-contract', symbol: 'Contract', alias: 'End', typeOnly: true }, + ]; + if (meta.from !== null) { + reqs.push({ + moduleSpecifier: './start-contract.json', + symbol: 'startContract', + kind: 'default', + attributes: { type: 'json' }, + }); + reqs.push({ + moduleSpecifier: './start-contract', + symbol: 'Contract', + alias: 'Start', + typeOnly: true, + }); + } + return reqs; } function indent(text: string, spaces: number): string { diff --git a/packages/3-targets/3-targets/postgres/test/migrations/render-typescript.test.ts b/packages/3-targets/3-targets/postgres/test/migrations/render-typescript.test.ts new file mode 100644 index 0000000000..73e3addde4 --- /dev/null +++ b/packages/3-targets/3-targets/postgres/test/migrations/render-typescript.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { CreateSchemaCall, DropTableCall } from '../../src/core/migrations/op-factory-call'; +import { renderCallsToTypeScript } from '../../src/core/migrations/render-typescript'; + +const renderTypeScript = ( + calls: Parameters[0], + meta: Parameters[1], +) => renderCallsToTypeScript(calls, meta); + +describe('renderCallsToTypeScript (postgres)', () => { + it('emits contract-JSON imports + fields and Migration header (with-start)', () => { + const output = renderTypeScript([new CreateSchemaCall('app')], { + from: 'sha256:aaa', + to: 'sha256:bbb', + }); + + expect(output).toContain( + "import { Migration, MigrationCLI } from '@prisma-next/postgres/migration';", + ); + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain( + 'import startContract from \'./start-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + expect(output).toContain("import type { Contract as Start } from './start-contract';"); + expect(output).toContain('export default class M extends Migration {'); + expect(output).toContain('override readonly startContractJson = startContract;'); + expect(output).toContain('override readonly endContractJson = endContract;'); + expect(output).toContain('override get operations()'); + expect(output).toContain('MigrationCLI.run(import.meta.url, M);'); + }); + + it('does NOT emit a describe() method (the base derives it from the contract JSON)', () => { + const output = renderTypeScript([new DropTableCall('public', 'stale')], { + from: 'sha256:aaa', + to: 'sha256:bbb', + }); + + expect(output).not.toContain('describe()'); + expect(output).not.toContain('sha256:aaa'); + expect(output).not.toContain('sha256:bbb'); + }); + + it('renders the baseline shape for from: null (no start imports, Migration)', () => { + const output = renderTypeScript([new CreateSchemaCall('app')], { + from: null, + to: 'sha256:bbb', + }); + + expect(output).toContain('export default class M extends Migration {'); + expect(output).toContain('override readonly endContractJson = endContract;'); + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + expect(output).not.toContain('start-contract'); + expect(output).not.toContain('startContractJson'); + expect(output).not.toContain('describe()'); + }); + + it('inlines the operation calls unchanged', () => { + const output = renderTypeScript([new CreateSchemaCall('app')], { + from: null, + to: 'sha256:bbb', + }); + expect(output).toContain('this.createSchema({ schema: "app" })'); + }); +}); diff --git a/packages/3-targets/3-targets/sqlite/src/core/migrations/render-typescript.ts b/packages/3-targets/3-targets/sqlite/src/core/migrations/render-typescript.ts index 1c8c1c2fba..3e1629015f 100644 --- a/packages/3-targets/3-targets/sqlite/src/core/migrations/render-typescript.ts +++ b/packages/3-targets/3-targets/sqlite/src/core/migrations/render-typescript.ts @@ -35,15 +35,19 @@ export function renderCallsToTypeScript( calls: ReadonlyArray, meta: RenderMigrationMeta, ): string { - const imports = buildImports(calls); + const imports = buildImports(calls, meta); const operationsBody = calls.map((c) => c.renderTypeScript()).join(',\n'); + const hasStart = meta.from !== null; + const startField = hasStart ? [' override readonly startContractJson = startContract;'] : []; return [ shebangLineFor(detectScaffoldRuntime()), imports, '', - 'export default class M extends Migration {', - buildDescribeMethod(meta), + `export default class M extends Migration<${hasStart ? 'Start' : 'never'}, End> {`, + ...startField, + ' override readonly endContractJson = endContract;', + '', ' override get operations() {', ' return [', indent(operationsBody, 6), @@ -56,8 +60,8 @@ export function renderCallsToTypeScript( ].join('\n'); } -function buildImports(calls: ReadonlyArray): string { - const requirements: ImportRequirement[] = [...BASE_IMPORTS]; +function buildImports(calls: ReadonlyArray, meta: RenderMigrationMeta): string { + const requirements: ImportRequirement[] = [...BASE_IMPORTS, ...contractImports(meta)]; for (const call of calls) { for (const req of call.importRequirements()) { requirements.push(req); @@ -66,16 +70,39 @@ function buildImports(calls: ReadonlyArray): string { return renderImports(requirements); } -function buildDescribeMethod(meta: RenderMigrationMeta): string { - const lines: string[] = []; - lines.push(' override describe() {'); - lines.push(' return {'); - lines.push(` from: ${JSON.stringify(meta.from)},`); - lines.push(` to: ${JSON.stringify(meta.to)},`); - lines.push(' };'); - lines.push(' }'); - lines.push(''); - return lines.join('\n'); +/** + * The committed contract-JSON imports the scaffold reads its from/to identity + * from. `end-contract.json` is always present; `start-contract.json` is added + * only for a non-baseline migration (`meta.from !== null`). The matching + * `Contract` type imports (aliased `Start`/`End`) feed the + * `Migration` generics. Baseline emits `Migration` with + * no start imports — `never` is the honest "no prior contract" Start. + */ +function contractImports(meta: RenderMigrationMeta): readonly ImportRequirement[] { + const reqs: ImportRequirement[] = [ + { + moduleSpecifier: './end-contract.json', + symbol: 'endContract', + kind: 'default', + attributes: { type: 'json' }, + }, + { moduleSpecifier: './end-contract', symbol: 'Contract', alias: 'End', typeOnly: true }, + ]; + if (meta.from !== null) { + reqs.push({ + moduleSpecifier: './start-contract.json', + symbol: 'startContract', + kind: 'default', + attributes: { type: 'json' }, + }); + reqs.push({ + moduleSpecifier: './start-contract', + symbol: 'Contract', + alias: 'Start', + typeOnly: true, + }); + } + return reqs; } function indent(text: string, spaces: number): string { diff --git a/packages/3-targets/3-targets/sqlite/test/migrations/planner.authoring-surface.test.ts b/packages/3-targets/3-targets/sqlite/test/migrations/planner.authoring-surface.test.ts index 142d8260ad..bf7c3c3838 100644 --- a/packages/3-targets/3-targets/sqlite/test/migrations/planner.authoring-surface.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/migrations/planner.authoring-surface.test.ts @@ -170,10 +170,15 @@ describe('SqliteMigrationPlanner authoring surface', () => { const source = result.plan.renderTypeScript(); expect(source).toContain("from '@prisma-next/sqlite/migration'"); expect(source).toMatch(/\bMigration\b/); - expect(source).toContain('export default class M extends Migration'); - expect(source).toContain(`from: "${coreHash('sha256:from')}"`); - expect(source).toContain(`to: "${createContract().storage.storageHash}"`); - expect(source).toContain('createTable('); + // New shape: base derives describe() from the imported contract JSON, so + // the scaffold carries `Migration` + the JSON/field imports + // and emits no `describe()` / hash literals. + expect(source).toContain('export default class M extends Migration'); + expect(source).toContain('override readonly startContractJson = startContract;'); + expect(source).toContain('override readonly endContractJson = endContract;'); + expect(source).not.toContain('describe()'); + expect(source).not.toContain(coreHash('sha256:from')); + expect(source).toContain('this.createTable('); }); }); @@ -194,7 +199,7 @@ describe('SqliteMigrationPlanner authoring surface', () => { expect(empty.destination).toEqual({ storageHash: 'sha256:to' }); }); - it('renders a stub whose describe() carries from/to and whose operations list is empty', () => { + it('renders a stub that derives from/to from contract JSON and has an empty operations list', () => { const planner = createSqliteMigrationPlanner(stubLowerer); const empty = planner.emptyMigration( { @@ -207,9 +212,13 @@ describe('SqliteMigrationPlanner authoring surface', () => { const source = empty.renderTypeScript(); expect(source).toContain("from '@prisma-next/sqlite/migration'"); - expect(source).toContain('export default class M extends Migration'); - expect(source).toContain('from: "sha256:from"'); - expect(source).toContain('to: "sha256:to"'); + // New shape: base derives from/to; scaffold imports the contract JSON + // rather than embedding hash literals or a describe() method. + expect(source).toContain('export default class M extends Migration'); + expect(source).toContain('override readonly endContractJson = endContract;'); + expect(source).not.toContain('describe()'); + expect(source).not.toContain('"sha256:from"'); + expect(source).not.toContain('"sha256:to"'); expect(source).toContain('override get operations()'); }); }); diff --git a/packages/3-targets/3-targets/sqlite/test/migrations/render-typescript.test.ts b/packages/3-targets/3-targets/sqlite/test/migrations/render-typescript.test.ts new file mode 100644 index 0000000000..3c1f722ace --- /dev/null +++ b/packages/3-targets/3-targets/sqlite/test/migrations/render-typescript.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { DropTableCall } from '../../src/core/migrations/op-factory-call'; +import { renderCallsToTypeScript } from '../../src/core/migrations/render-typescript'; + +const renderTypeScript = ( + calls: Parameters[0], + meta: Parameters[1], +) => renderCallsToTypeScript(calls, meta); + +describe('renderCallsToTypeScript (sqlite)', () => { + it('emits contract-JSON imports + fields and Migration header (with-start)', () => { + const output = renderTypeScript([new DropTableCall('stale')], { + from: 'sha256:aaa', + to: 'sha256:bbb', + }); + + expect(output).toContain( + "import { Migration, MigrationCLI } from '@prisma-next/sqlite/migration';", + ); + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain( + 'import startContract from \'./start-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + expect(output).toContain("import type { Contract as Start } from './start-contract';"); + expect(output).toContain('export default class M extends Migration {'); + expect(output).toContain('override readonly startContractJson = startContract;'); + expect(output).toContain('override readonly endContractJson = endContract;'); + expect(output).toContain('override get operations()'); + expect(output).toContain('MigrationCLI.run(import.meta.url, M);'); + }); + + it('does NOT emit a describe() method (the base derives it from the contract JSON)', () => { + const output = renderTypeScript([new DropTableCall('stale')], { + from: 'sha256:aaa', + to: 'sha256:bbb', + }); + + expect(output).not.toContain('describe()'); + expect(output).not.toContain('sha256:aaa'); + expect(output).not.toContain('sha256:bbb'); + }); + + it('renders the baseline shape for from: null (no start imports, Migration)', () => { + const output = renderTypeScript([new DropTableCall('stale')], { + from: null, + to: 'sha256:bbb', + }); + + expect(output).toContain('export default class M extends Migration {'); + expect(output).toContain('override readonly endContractJson = endContract;'); + expect(output).toContain( + 'import endContract from \'./end-contract.json\' with { type: "json" };', + ); + expect(output).toContain("import type { Contract as End } from './end-contract';"); + expect(output).not.toContain('start-contract'); + expect(output).not.toContain('startContractJson'); + expect(output).not.toContain('describe()'); + }); + + it('inlines the operation calls unchanged', () => { + const output = renderTypeScript([new DropTableCall('stale')], { + from: null, + to: 'sha256:bbb', + }); + expect(output).toContain('this.dropTable({ table: "stale" })'); + }); +}); diff --git a/packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts b/packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts index 53dcb07bbe..00a6639b6c 100644 --- a/packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts @@ -406,11 +406,16 @@ describe('renderCallsToTypeScript', () => { ); }); - it('embeds describe() with the supplied from/to hashes', () => { + it('derives describe() from contract JSON instead of embedding from/to hashes', () => { const source = renderCallsToTypeScript([], { from: 'sha256:a', to: 'sha256:b' }); - expect(source).toContain('from: "sha256:a",'); - expect(source).toContain('to: "sha256:b",'); - expect(source).toContain('export default class M extends Migration {'); + // New shape: from/to are derived by the base from the imported contract JSON + // (no describe() block, no hash literals). + expect(source).not.toContain('describe()'); + expect(source).not.toContain('sha256:a'); + expect(source).not.toContain('sha256:b'); + expect(source).toContain('export default class M extends Migration {'); + expect(source).toContain('override readonly startContractJson = startContract;'); + expect(source).toContain('override readonly endContractJson = endContract;'); expect(source).toContain( "import { Migration, MigrationCLI } from '@prisma-next/postgres/migration';", ); diff --git a/packages/3-targets/6-adapters/postgres/test/migrations/planner.authoring-surface.test.ts b/packages/3-targets/6-adapters/postgres/test/migrations/planner.authoring-surface.test.ts index 5b1fc9dfbb..1109cf73d7 100644 --- a/packages/3-targets/6-adapters/postgres/test/migrations/planner.authoring-surface.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/migrations/planner.authoring-surface.test.ts @@ -103,9 +103,13 @@ describe('PostgresMigrationPlanner authoring surface', () => { expect(source).toContain("from '@prisma-next/postgres/migration'"); expect(source).toMatch(/\bMigration\b/); - expect(source).toContain('export default class M extends Migration'); - expect(source).toContain(`from: "${coreHash('sha256:from')}"`); - expect(source).toContain(`to: "${contract.storage.storageHash}"`); + // New shape: base derives describe() from the imported contract JSON, so + // the scaffold carries `Migration` + the JSON/field imports + // and emits no describe()/hash literals. + expect(source).toContain('export default class M extends Migration'); + expect(source).toContain('override readonly endContractJson = endContract;'); + expect(source).not.toContain('describe()'); + expect(source).not.toContain(coreHash('sha256:from')); }); }); @@ -126,7 +130,7 @@ describe('PostgresMigrationPlanner authoring surface', () => { expect(empty.destination).toEqual({ storageHash: 'sha256:to' }); }); - it('renders a stub whose describe() carries from/to and whose operations list is empty', () => { + it('renders a stub that derives from/to from contract JSON and has an empty operations list', () => { const planner = makeFrameworkPlanner(); const empty = planner.emptyMigration( { @@ -140,9 +144,11 @@ describe('PostgresMigrationPlanner authoring surface', () => { const source = empty.renderTypeScript(); expect(source).toContain("from '@prisma-next/postgres/migration'"); - expect(source).toContain('export default class M extends Migration'); - expect(source).toContain('from: "sha256:from"'); - expect(source).toContain('to: "sha256:to"'); + expect(source).toContain('export default class M extends Migration'); + expect(source).toContain('override readonly endContractJson = endContract;'); + expect(source).not.toContain('describe()'); + expect(source).not.toContain('"sha256:from"'); + expect(source).not.toContain('"sha256:to"'); expect(source).toContain('override get operations()'); }); }); diff --git a/packages/3-targets/6-adapters/postgres/test/migrations/render-typescript.roundtrip.test.ts b/packages/3-targets/6-adapters/postgres/test/migrations/render-typescript.roundtrip.test.ts index 6d7fe3f9f6..dd3a1d612b 100644 --- a/packages/3-targets/6-adapters/postgres/test/migrations/render-typescript.roundtrip.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/migrations/render-typescript.roundtrip.test.ts @@ -100,6 +100,33 @@ function rewriteImports(tsSource: string): string { ); } +/** + * Write the committed contract fixtures the rendered scaffold imports — + * `{start,end}-contract.json` (carrying `storage.storageHash`, which the base's + * derived `describe()` reads) and the matching `{start,end}-contract.ts` type + * modules (`export type Contract`). The JSON hashes match `meta` so the derived + * describe() is consistent with the migration's identity. + */ +async function writeContractFixtures( + dir: string, + meta: { readonly from: string | null; readonly to: string }, +): Promise { + const contractType = + 'export type Contract = { readonly storage: { readonly storageHash: string } };\n'; + await writeFile( + join(dir, 'end-contract.json'), + JSON.stringify({ storage: { storageHash: meta.to } }, null, 2), + ); + await writeFile(join(dir, 'end-contract.ts'), contractType); + if (meta.from !== null) { + await writeFile( + join(dir, 'start-contract.json'), + JSON.stringify({ storage: { storageHash: meta.from } }, null, 2), + ); + await writeFile(join(dir, 'start-contract.ts'), contractType); + } +} + const META = { from: 'sha256:0000000000000000000000000000000000000000000000000000000000000000', to: 'sha256:1111111111111111111111111111111111111111111111111111111111111111', @@ -112,6 +139,12 @@ describe('TypeScriptRenderablePostgresMigration round-trip', () => { tmpDir = await mkdtemp(join(tmpdir(), 'postgres-render-roundtrip-')); await writeFile(join(tmpDir, 'package.json'), '{"type":"module"}'); await writeFile(join(tmpDir, 'prisma-next.config.ts'), fixtureConfigSource); + // The rendered scaffold imports its from/to identity from committed + // contract JSON (the base derives describe() from `storage.storageHash`) + // and the matching `Contract` types. Write minimal fixtures so the + // executed migration resolves its imports; the from/to hashes here match + // META so the derived describe() is consistent. + await writeContractFixtures(tmpDir, META); }); afterEach(async () => { diff --git a/packages/3-targets/6-adapters/sqlite/test/migrations/render-typescript.roundtrip.test.ts b/packages/3-targets/6-adapters/sqlite/test/migrations/render-typescript.roundtrip.test.ts index 392bb6ef75..4393fdfa0d 100644 --- a/packages/3-targets/6-adapters/sqlite/test/migrations/render-typescript.roundtrip.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/migrations/render-typescript.roundtrip.test.ts @@ -89,6 +89,32 @@ function rewriteImports(tsSource: string): string { return tsSource.replace("'@prisma-next/sqlite/migration'", `'${targetSqliteMigrationExport}'`); } +/** + * Write the committed contract fixtures the rendered scaffold imports — + * `{start,end}-contract.json` (carrying `storage.storageHash`, which the base's + * derived `describe()` reads) and the matching `{start,end}-contract.ts` type + * modules. The JSON hashes match `meta` so the derived describe() is consistent. + */ +async function writeContractFixtures( + dir: string, + meta: { readonly from: string | null; readonly to: string }, +): Promise { + const contractType = + 'export type Contract = { readonly storage: { readonly storageHash: string } };\n'; + await writeFile( + join(dir, 'end-contract.json'), + JSON.stringify({ storage: { storageHash: meta.to } }, null, 2), + ); + await writeFile(join(dir, 'end-contract.ts'), contractType); + if (meta.from !== null) { + await writeFile( + join(dir, 'start-contract.json'), + JSON.stringify({ storage: { storageHash: meta.from } }, null, 2), + ); + await writeFile(join(dir, 'start-contract.ts'), contractType); + } +} + const META = { from: 'sha256:0000000000000000000000000000000000000000000000000000000000000000', to: 'sha256:1111111111111111111111111111111111111111111111111111111111111111', @@ -105,6 +131,11 @@ describe('TypeScriptRenderableSqliteMigration round-trip', () => { tmpDir = await mkdtemp(join(tmpdir(), 'sqlite-render-roundtrip-')); await writeFile(join(tmpDir, 'package.json'), '{"type":"module"}'); await writeFile(join(tmpDir, 'prisma-next.config.ts'), fixtureConfigSource); + // The rendered scaffold imports its from/to identity from committed + // contract JSON (the base derives describe() from `storage.storageHash`) + // plus the matching `Contract` types. Write minimal fixtures so the + // executed migration resolves its imports. + await writeContractFixtures(tmpDir, META); }); afterEach(async () => { From 7c4973a7b313198f68ff3373bfc86af9946dc5bf Mon Sep 17 00:00:00 2001 From: willbot Date: Mon, 29 Jun 2026 19:07:52 +0200 Subject: [PATCH 08/12] fix(tml-2892): regen-script + cast-plugin + integration-helper for the new migration shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` scaffold - record the extension-author upgrade-coverage entry for the unboundNamespace lift Co-Authored-By: Claude Opus 4.8 Signed-off-by: willbot Signed-off-by: Will Madden --- biome-plugins/no-bare-cast.grit | 41 ++++++++++--------- .../src/core/render-typescript.ts | 22 ++++++---- scripts/regen-example-migrations.mjs | 21 +++++++++- .../upgrades/0.14-to-0.15/instructions.md | 10 +++++ .../test/utils/journey-test-helpers.ts | 23 ++++++++--- 5 files changed, 83 insertions(+), 34 deletions(-) diff --git a/biome-plugins/no-bare-cast.grit b/biome-plugins/no-bare-cast.grit index 37f5d92f61..999acbfc34 100644 --- a/biome-plugins/no-bare-cast.grit +++ b/biome-plugins/no-bare-cast.grit @@ -1,28 +1,29 @@ -// Recognise every `as` token except `as const`, and skip test files. +// Recognise value-level `as` casts and skip `as const` + test files. // -// The pattern is at the top level (not wrapped in `file(...)`) so it iterates -// over every match in the file — wrapping in `file($name, $body) where { $body -// <: contains }` only fires on the first occurrence, which would -// undercount the cast total the CI ratchet checks. +// Anchored on the `TsAsExpression` node (the AST node for a value-level +// `expr as Type` assertion) rather than a bare `$x as $t` token pattern. The +// token pattern also matched the `as` in import-specifier / re-export aliases +// (`import type { Contract as End }`, `export { X as Y }`), which are NOT +// type-assertion casts. The migration-scaffold generator (TML-2892) emits +// `import type { Contract as End }` / `as Start`, so the token pattern produced +// false positives that tripped the CI cast ratchet (scripts/lint-casts.mjs). +// `TsAsExpression` only matches real casts, so import aliases are not counted. // -// `$filename` is biome's built-in for the current file path. Biome's GritQL -// regex matching is anchored full-match, so patterns need a `.*` prefix. -// The three exclusion regexes mirror biome's existing test-file `overrides § -// includes` patterns (`**/*.test.ts`, `**/*.test-d.ts`, `**/test/**/*.ts`). +// `$ty` is the target-type child; `as const` is excluded by the `^const$` +// guard. `$filename` is biome's built-in current-file path; biome's GritQL +// regex matching is anchored full-match, so patterns need a `.*` prefix. The +// three file-exclusion regexes mirror biome's test-file override includes +// (`**/*.test.ts`, `**/*.test-d.ts`, `**/test/**/*.ts`). // -// The message avoids escape sequences (e.g. `\"`) because biome's GritQL -// engine appears to mis-parse subsequent characters in the string when an -// escape sequence is present (`r` → CR, `t` → TAB, `n` → LF, …). -// -// Severity is "info" so the plugin can run repo-wide without breaking -// existing builds (the codebase has thousands of pre-existing `as` casts -// that this rule reports). The CI ratchet enforces the actual gate by -// counting diagnostics from biome's JSON reporter; "info" entries still +// Severity is "info" so the plugin runs repo-wide without breaking existing +// builds (thousands of pre-existing casts). The CI ratchet enforces the gate by +// counting these diagnostics from biome's JSON reporter; "info" entries still // appear there with the distinctive `no-bare-cast: ` prefix. +language js -`$x as $t` where { - not $t <: r"^const$", +TsAsExpression(ty=$ty) as $cast where { + not $ty <: r"^const$", not $filename <: r".*\.(?:test|test-d)\.ts", not $filename <: r".*/test/.*\.ts", - register_diagnostic(span=$x, message="no-bare-cast: bare `as` cast; replace with blindCast(...) or castAs(value), or rewrite to eliminate the cast", severity="info") + register_diagnostic(span=$cast, message="no-bare-cast: bare `as` cast; replace with blindCast(...) or castAs(value), or rewrite to eliminate the cast", severity="info") } diff --git a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts index 20235d8830..fa8cc31001 100644 --- a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts +++ b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts @@ -32,18 +32,26 @@ const BASE_IMPORTS: readonly ImportRequirement[] = [ * Render a list of Mongo `OpFactoryCall`s as a `migration.ts` * source string. The result is shebanged, extends the user-facing * `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and - * implements the abstract `operations` and `describe` members. `meta` is - * always rendered — `describe()` is part of the `Migration` contract, so - * even an empty stub must satisfy it; callers pass `from: null` for a - * baseline `migration-new` scaffold (and a real `to` hash either way). + * implements `operations`. + * + * The scaffold does NOT render a `describe()` method. Instead it imports the + * committed contract JSON (`end-contract.json`, plus `start-contract.json` for a + * non-baseline migration) and assigns it to the `endContractJson` / + * `startContractJson` fields; the `Migration` base derives `describe()`'s + * from/to from those JSONs' `storage.storageHash`. The class is parameterised + * `Migration` (or `Migration` for a baseline, where + * `meta.from === null` — no start contract). `meta.to` is no longer embedded as + * a literal (the base reads it from the JSON at runtime); `meta.from` only + * selects the baseline vs non-baseline shape. * * The walk is polymorphic: each call node contributes its own * `renderTypeScript()` expression and declares its own * `importRequirements()`. The top-level renderer aggregates imports * across all nodes and emits one `import { … } from "…"` line per module. - * The `Migration` and `MigrationCLI` imports are always emitted — they're - * structural to the rendered scaffold (extends `Migration`, calls - * `MigrationCLI.run`), not driven by any node. + * The `Migration` / `MigrationCLI` base imports and the contract-JSON imports + * are always emitted — they're structural to the rendered scaffold (extends + * `Migration`, derives `describe()` from the JSON, calls `MigrationCLI.run`), + * not driven by any node. */ export function renderCallsToTypeScript( calls: ReadonlyArray, diff --git a/scripts/regen-example-migrations.mjs b/scripts/regen-example-migrations.mjs index fdbf65c90b..9cd72b9a88 100644 --- a/scripts/regen-example-migrations.mjs +++ b/scripts/regen-example-migrations.mjs @@ -136,11 +136,29 @@ function biomeFormatInPlace(filePath) { * `newFromHash` is null for baseline migrations (whose `from:` is `null`). * For non-baseline migrations both `from:` and `to:` are updated. * + * New-shape migrations (post TML-2892) carry no hash literals: they import the + * committed `end-contract.json` / `start-contract.json` and the `Migration` base + * derives `describe()`'s from/to from those JSONs' `storage.storageHash`. The + * regen pipeline re-emits those contract JSONs upstream of this call, so for the + * new shape there is nothing to rewrite here — the correct hashes already live + * in the regenerated JSON. Detect that shape (`endContractJson = endContract` + * with no `to: 'sha256:...'` literal) and skip the rewrite. + * * Returns true if the file was changed, false if the hashes were already - * up-to-date. + * up-to-date (or the file is the new contract-JSON shape). */ function rewriteMigrationHashes(migrationTsPath, newFromHash, newToHash) { const src = readFileSync(migrationTsPath, 'utf8'); + + const toPattern = /(to:\s*['"])sha256:[0-9a-f]+(['"])/g; + const isContractJsonShape = + src.includes('endContractJson = endContract') && [...src.matchAll(toPattern)].length === 0; + if (isContractJsonShape) { + // The from/to identity is derived by the base from the (already-regenerated) + // contract JSON; no literal to rewrite in migration.ts. + return false; + } + let updated = src; if (newFromHash !== null) { @@ -159,7 +177,6 @@ function rewriteMigrationHashes(migrationTsPath, newFromHash, newToHash) { updated = updated.replace(fromPattern, `$1${newFromHash}$2`); } - const toPattern = /(to:\s*['"])sha256:[0-9a-f]+(['"])/g; const toMatches = [...updated.matchAll(toPattern)]; if (toMatches.length === 0) { throw new Error(`regen-example-migrations: no 'to: sha256:...' literal in ${migrationTsPath}`); diff --git a/skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md b/skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md index fd8a27e3ed..b259d909c2 100644 --- a/skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md +++ b/skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md @@ -166,3 +166,13 @@ released surface changed (the `WithExtensionNamespaces` export existed only on t branch's earlier, unmerged merge-design revision and was removed before merge). No extension-author API change. Incidental substrate diff only. --> + + diff --git a/test/integration/test/utils/journey-test-helpers.ts b/test/integration/test/utils/journey-test-helpers.ts index 7bd5cba27c..f8b8f68723 100644 --- a/test/integration/test/utils/journey-test-helpers.ts +++ b/test/integration/test/utils/journey-test-helpers.ts @@ -437,12 +437,26 @@ export async function runMigrationCheck( return runCommand(createMigrationCheckCommand(), ctx, extraArgs); } +// The generator emits `import endContract from './end-contract.json' with { type: "json" };` +// (double-quoted attribute value via JSON.stringify). Match either quote style +// so the helper is robust to formatting. const END_CONTRACT_JSON_IMPORT_RE = - /import endContract from '\.\/end-contract\.json' with \{ type: 'json' \};\r?\n/; + /import endContract from '\.\/end-contract\.json' with \{ type: ["']json["'] \};\r?\n/; + +// The generator's class header carries the `` / `` +// generics (post TML-2892). Match the header up to the opening brace. +const MIGRATION_CLASS_HEADER_RE = /export default class M extends Migration(?:<[^>]*>)? \{/; /** - * Planner scaffolds import raw `end-contract.json` as `endContract`. Runtime SQL - * qualification requires a hydrated Postgres schema namespace (`qualifyTable`). + * Planner scaffolds import raw `end-contract.json` as `endContract` and derive + * their from/to from it via the `Migration` base. This helper rewrites the + * scaffold for the journey test's runtime apply: it drops the raw-JSON import, + * deserializes the contract (runtime SQL qualification needs a hydrated Postgres + * schema namespace with `qualifyTable`), and wires a `db = sql(...)` the + * filled-in dataTransform closures use. The deserialized contract is bound back + * to `endContract`, so the scaffold's `endContractJson = endContract` field + * still resolves (and the base still derives `describe()` from its + * `storage.storageHash`). */ export function injectMigrationSqlDbSetup(scaffold: string): string { const block = [ @@ -462,11 +476,10 @@ export function injectMigrationSqlDbSetup(scaffold: string): string { ' }),', '});', '', - 'export default class M extends Migration {', ].join('\n'); return scaffold .replace(END_CONTRACT_JSON_IMPORT_RE, '') - .replace('export default class M extends Migration {', block); + .replace(MIGRATION_CLASS_HEADER_RE, (header) => `${block}${header}`); } /** From 44c491b22c52afb4304c83aa3ed1f7671ca3055d Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 1 Jul 2026 14:16:07 +0200 Subject: [PATCH 09/12] feat(tml-2892): regenerate all example migrations to the contract-JSON shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every example migration (70) now uses the generator's contract-JSON shape — contract-JSON imports, `Migration` (baseline ``), 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../app/20260409T1030_migration/migration.ts | 11 +- .../migration.ts | 14 +- .../migration.ts | 14 +- .../migration.ts | 11 +- .../migration.ts | 11 +- .../app/20260301T1000_init/migration.ts | 11 +- .../app/20260302T1000_add_phone/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1100_add_posts/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1200_add_avatar/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../20260303T1000_merge_phone/migration.ts | 14 +- .../start-contract.d.ts | 217 ++++++++++++ .../start-contract.json | 145 ++++++++ .../20260303T1100_merge_posts/migration.ts | 14 +- .../start-contract.d.ts | 217 ++++++++++++ .../start-contract.json | 145 ++++++++ .../20260303T1200_merge_avatar/migration.ts | 14 +- .../start-contract.d.ts | 217 ++++++++++++ .../start-contract.json | 145 ++++++++ .../app/20260301T1000_init/migration.ts | 11 +- .../migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../20260302T1100_bob_add_avatar/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../20260303T1000_merge_alice/migration.ts | 14 +- .../start-contract.d.ts | 217 ++++++++++++ .../start-contract.json | 145 ++++++++ .../app/20260303T1100_merge_bob/migration.ts | 14 +- .../start-contract.d.ts | 217 ++++++++++++ .../start-contract.json | 145 ++++++++ .../app/20260301T1000_init/migration.ts | 11 +- .../app/20260302T1000_add_phone/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260303T1000_add_bio/migration.ts | 14 +- .../20260303T1000_add_bio/start-contract.d.ts | 217 ++++++++++++ .../20260303T1000_add_bio/start-contract.json | 145 ++++++++ .../app/20260304T1000_add_posts/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260305T1000_add_avatar/migration.ts | 14 +- .../start-contract.d.ts | 251 ++++++++++++++ .../start-contract.json | 175 ++++++++++ .../20260306T1000_add_comments/migration.ts | 14 +- .../start-contract.d.ts | 268 +++++++++++++++ .../start-contract.json | 190 +++++++++++ .../app/20260307T1000_add_tags/migration.ts | 14 +- .../start-contract.d.ts | 285 ++++++++++++++++ .../start-contract.json | 205 ++++++++++++ .../20260307T1100_late_branch/migration.ts | 14 +- .../start-contract.d.ts | 302 +++++++++++++++++ .../start-contract.json | 220 ++++++++++++ .../20260308T1000_add_everything/migration.ts | 14 +- .../start-contract.d.ts | 302 +++++++++++++++++ .../start-contract.json | 220 ++++++++++++ .../app/20260301T1000_init/migration.ts | 11 +- .../app/20260302T1000_add_phone/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1100_add_posts/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1200_add_avatar/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260303T1000_add_bio/migration.ts | 14 +- .../20260303T1000_add_bio/start-contract.d.ts | 217 ++++++++++++ .../20260303T1000_add_bio/start-contract.json | 145 ++++++++ .../app/20260304T1000_parallel_a/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260304T1000_parallel_b/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260304T1000_parallel_c/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260304T1000_parallel_d/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260601T0719_init/migration.ts | 11 +- .../app/20260601T0725_add_name/migration.ts | 14 +- .../start-contract.d.ts | 167 ++++++++++ .../start-contract.json | 130 ++++++++ .../20260601T0725_alice_phone/migration.ts | 14 +- .../start-contract.d.ts | 182 ++++++++++ .../start-contract.json | 145 ++++++++ .../app/20260601T0725_bob_avatar/migration.ts | 14 +- .../start-contract.d.ts | 182 ++++++++++ .../start-contract.json | 145 ++++++++ .../app/20260601T0726_add_bio/migration.ts | 14 +- .../20260601T0726_add_bio/start-contract.d.ts | 206 ++++++++++++ .../20260601T0726_add_bio/start-contract.json | 175 ++++++++++ .../app/20260601T0726_add_locale/migration.ts | 14 +- .../start-contract.d.ts | 218 ++++++++++++ .../start-contract.json | 190 +++++++++++ .../20260601T0726_fast_forward/migration.ts | 14 +- .../start-contract.d.ts | 167 ++++++++++ .../start-contract.json | 130 ++++++++ .../20260601T0726_merge_alice/migration.ts | 14 +- .../start-contract.d.ts | 194 +++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260601T0726_merge_bob/migration.ts | 14 +- .../start-contract.d.ts | 194 +++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260601T0727_hotfix/migration.ts | 14 +- .../20260601T0727_hotfix/start-contract.d.ts | 230 +++++++++++++ .../20260601T0727_hotfix/start-contract.json | 205 ++++++++++++ .../20260601T0727_rollback_alice/migration.ts | 14 +- .../start-contract.d.ts | 194 +++++++++++ .../start-contract.json | 160 +++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 230 +++++++++++++ .../start-contract.json | 205 ++++++++++++ .../20260601T0727_rollback_users/migration.ts | 14 +- .../start-contract.d.ts | 230 +++++++++++++ .../start-contract.json | 205 ++++++++++++ .../20260601T0728_promote_bob/migration.ts | 14 +- .../start-contract.d.ts | 194 +++++++++++ .../start-contract.json | 160 +++++++++ .../start-contract.d.ts | 246 ++++++++++++++ .../start-contract.json | 224 +++++++++++++ .../app/20260601T0730_experiment/migration.ts | 14 +- .../start-contract.d.ts | 299 +++++++++++++++++ .../start-contract.json | 297 +++++++++++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 314 ++++++++++++++++++ .../start-contract.json | 312 +++++++++++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 246 ++++++++++++++ .../start-contract.json | 224 +++++++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 218 ++++++++++++ .../start-contract.json | 190 +++++++++++ .../app/20260301T1000_init/migration.ts | 11 +- .../app/20260302T1000_add_phone/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260303T1000_add_bio/migration.ts | 14 +- .../20260303T1000_add_bio/start-contract.d.ts | 217 ++++++++++++ .../20260303T1000_add_bio/start-contract.json | 145 ++++++++ .../app/20260304T1000_add_posts/migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 251 ++++++++++++++ .../start-contract.json | 175 ++++++++++ .../migration.ts | 14 +- .../start-contract.d.ts | 234 +++++++++++++ .../start-contract.json | 160 +++++++++ .../app/20260301T1000_init/migration.ts | 11 +- .../app/20260302T1000_add_phone/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1100_add_posts/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260302T1200_add_avatar/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../20260302T1300_add_category/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../20260302T1400_add_settings/migration.ts | 14 +- .../start-contract.d.ts | 197 +++++++++++ .../start-contract.json | 130 ++++++++ .../app/20260422T0720_initial/migration.ts | 11 +- .../app/20260512T1309_migration/migration.ts | 11 +- .../app/20260513T0505_initial/migration.ts | 303 +++++++++++++++-- .../migration.ts | 14 +- scripts/codemod-migration-shape.mjs | 135 ++++++++ .../cli-journeys/mongo-migration.e2e.test.ts | 8 +- 179 files changed, 21585 insertions(+), 508 deletions(-) create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.json create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.d.ts create mode 100644 examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.json create mode 100644 scripts/codemod-migration-shape.mjs diff --git a/examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts b/examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts index 415d23e3b0..bc73f45036 100644 --- a/examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts +++ b/examples/mongo-demo/migrations/app/20260409T1030_migration/migration.ts @@ -1,14 +1,11 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createIndex } from '@prisma-next/target-mongo/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -class InitialMigration extends Migration { - override describe() { - return { - from: null, - to: 'sha256:2827cbad7293fe13a4fb2aab60a55d3cddd856a86d1f6ccea6e11519faacff92', - }; - } +class InitialMigration extends Migration { + override readonly endContractJson = endContract; override get operations() { return [createIndex('users', [{ field: 'email', direction: 1 }], { unique: true })]; diff --git a/examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/migration.ts b/examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/migration.ts index e29853af30..2ca7aec783 100755 --- a/examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/migration.ts +++ b/examples/mongo-demo/migrations/app/20260626T1605_add_user_role_enum/migration.ts @@ -2,14 +2,14 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { collMod } from '@prisma-next/target-mongo/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -class M extends Migration { - override describe() { - return { - from: 'sha256:2827cbad7293fe13a4fb2aab60a55d3cddd856a86d1f6ccea6e11519faacff92', - to: 'sha256:250af57beb0580c2c9562789d5d05ae39bcfabd08b2eca8367f59a70fa724b7d', - }; - } +class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/migration.ts b/examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/migration.ts index c268cda91b..0700c902c4 100755 --- a/examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/migration.ts +++ b/examples/mongo-demo/migrations/app/20260626T1916_add_posts_indexes/migration.ts @@ -2,14 +2,14 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createIndex } from '@prisma-next/target-mongo/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -class M extends Migration { - override describe() { - return { - from: 'sha256:250af57beb0580c2c9562789d5d05ae39bcfabd08b2eca8367f59a70fa724b7d', - to: 'sha256:ecc554e5f2f05ec120f8fef5ddf536286471edd3de11b8a906ba70e71f5e5df3', - }; - } +class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/multi-extension-monorepo/packages/audit/migrations/20260601T0000_create_audit_event/migration.ts b/examples/multi-extension-monorepo/packages/audit/migrations/20260601T0000_create_audit_event/migration.ts index e56184fbc6..c6844eff70 100755 --- a/examples/multi-extension-monorepo/packages/audit/migrations/20260601T0000_create_audit_event/migration.ts +++ b/examples/multi-extension-monorepo/packages/audit/migrations/20260601T0000_create_audit_event/migration.ts @@ -16,14 +16,11 @@ import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control' import { Migration, MigrationCLI, rawSql } from '@prisma-next/target-postgres/migration'; import type { PostgresPlanTargetDetails } from '@prisma-next/target-postgres/planner-target-details'; import { AUDIT_BASELINE_INVARIANT_ID, AUDIT_EVENT_TABLE } from '../../src/constants'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:b0d547223488b4a8cea642a0bb2cc8e8f6cd9b2a6e490f23832865146ac51468', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations(): readonly SqlMigrationPlanOperation[] { return [ diff --git a/examples/multi-extension-monorepo/packages/feature-flags/migrations/20260601T0000_create_feature_flag/migration.ts b/examples/multi-extension-monorepo/packages/feature-flags/migrations/20260601T0000_create_feature_flag/migration.ts index 19aede36e0..25496c967c 100755 --- a/examples/multi-extension-monorepo/packages/feature-flags/migrations/20260601T0000_create_feature_flag/migration.ts +++ b/examples/multi-extension-monorepo/packages/feature-flags/migrations/20260601T0000_create_feature_flag/migration.ts @@ -14,14 +14,11 @@ import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control' import { Migration, MigrationCLI, rawSql } from '@prisma-next/target-postgres/migration'; import type { PostgresPlanTargetDetails } from '@prisma-next/target-postgres/planner-target-details'; import { FEATURE_FLAG_TABLE, FEATURE_FLAGS_BASELINE_INVARIANT_ID } from '../../src/constants'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:6759a7591f2bdf9b5c20fcbf2b02dbf56956c7762cef663c5e8e2b6779057cf4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations(): readonly SqlMigrationPlanOperation[] { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1000_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/migration.ts index bdfc9830d1..7836faee7b 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('posts', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1100_add_posts/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/migration.ts index a836c7cb14..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260302T1200_add_avatar/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/migration.ts index 8b34a56f87..c609a6bd2f 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - to: 'sha256:042e80cecce8271bb96dedcc7986dadb4c72b098ad5b5b43d8f3bf2d5d61806b', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.d.ts new file mode 100644 index 0000000000..eb94201878 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.json new file mode 100644 index 0000000000..f939d86a45 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1000_merge_phone/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/migration.ts index caee9ba275..9c067e324e 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0', - to: 'sha256:042e80cecce8271bb96dedcc7986dadb4c72b098ad5b5b43d8f3bf2d5d61806b', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.d.ts new file mode 100644 index 0000000000..89d5f0fa6c --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.json new file mode 100644 index 0000000000..acdc551e18 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1100_merge_posts/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "posts": { + "column": "posts" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/migration.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/migration.ts index cc008148d5..376f1773db 100755 --- a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - to: 'sha256:042e80cecce8271bb96dedcc7986dadb4c72b098ad5b5b43d8f3bf2d5d61806b', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.d.ts new file mode 100644 index 0000000000..f991835c98 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.json new file mode 100644 index 0000000000..38691813bb --- /dev/null +++ b/examples/prisma-next-demo/fixtures/converging-branches/migrations/app/20260303T1200_merge_avatar/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1000_alice_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/migration.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/migration.ts index a836c7cb14..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260302T1100_bob_add_avatar/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/migration.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/migration.ts index 3966e3e7d5..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/migration.ts +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - to: 'sha256:f9a41d77df6eae57bcd25ab25df31e6e905aad034a5b813f408bf8e78e9f384a', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.d.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.d.ts new file mode 100644 index 0000000000..eb94201878 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.json b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.json new file mode 100644 index 0000000000..f939d86a45 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1000_merge_alice/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/migration.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/migration.ts index 977c539a85..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/migration.ts +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - to: 'sha256:f9a41d77df6eae57bcd25ab25df31e6e905aad034a5b813f408bf8e78e9f384a', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.d.ts b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.d.ts new file mode 100644 index 0000000000..f991835c98 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.json b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.json new file mode 100644 index 0000000000..38691813bb --- /dev/null +++ b/examples/prisma-next-demo/fixtures/diamond/migrations/app/20260303T1100_merge_bob/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260302T1000_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/migration.ts index 2116e2b66f..ac4493b8c3 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - to: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('bio', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.d.ts new file mode 100644 index 0000000000..eb94201878 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.json new file mode 100644 index 0000000000..f939d86a45 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260303T1000_add_bio/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/migration.ts index 0537d3aded..7836faee7b 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('posts', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260304T1000_add_posts/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/migration.ts index 8117a83813..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8', - to: 'sha256:47f4a4f58e99a479aaa3ba2ff9d00d1728b1c240359e03aa564ae163d63ba8d7', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.d.ts new file mode 100644 index 0000000000..c5da3fe1df --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.d.ts @@ -0,0 +1,251 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.json new file mode 100644 index 0000000000..af3a3d49a4 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260305T1000_add_avatar/start-contract.json @@ -0,0 +1,175 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/migration.ts index b76c9e3798..d6e69dd12d 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:47f4a4f58e99a479aaa3ba2ff9d00d1728b1c240359e03aa564ae163d63ba8d7', - to: 'sha256:b34dc9149b6856eacb47e3e6f2157860d8690f5fb382e71ae6b27f943007b778', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.d.ts new file mode 100644 index 0000000000..97412446a9 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.d.ts @@ -0,0 +1,268 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:47f4a4f58e99a479aaa3ba2ff9d00d1728b1c240359e03aa564ae163d63ba8d7'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.json new file mode 100644 index 0000000000..93cd2a5963 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260306T1000_add_comments/start-contract.json @@ -0,0 +1,190 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:47f4a4f58e99a479aaa3ba2ff9d00d1728b1c240359e03aa564ae163d63ba8d7" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/migration.ts index 9de184ff0a..182d830811 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:b34dc9149b6856eacb47e3e6f2157860d8690f5fb382e71ae6b27f943007b778', - to: 'sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('tags', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.d.ts new file mode 100644 index 0000000000..ef549f43b5 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.d.ts @@ -0,0 +1,285 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:b34dc9149b6856eacb47e3e6f2157860d8690f5fb382e71ae6b27f943007b778'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly comments: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly comments: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly comments: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.json new file mode 100644 index 0000000000..429f719610 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1000_add_tags/start-contract.json @@ -0,0 +1,205 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "comments": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "comments": { + "column": "comments" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "comments": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:b34dc9149b6856eacb47e3e6f2157860d8690f5fb382e71ae6b27f943007b778" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/migration.ts index 133b6be903..46986aa325 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84', - to: 'sha256:6c66c897ec186bbc7b69feb6bfc5bb19b28a4c9ffe1a4fff5c96172cf5716ea5', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.d.ts new file mode 100644 index 0000000000..7e17eb732a --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.d.ts @@ -0,0 +1,302 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly comments: CodecTypes['pg/text@1']['output'] | null; + readonly tags: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly comments: CodecTypes['pg/text@1']['input'] | null; + readonly tags: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly comments: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly tags: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly tags: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + readonly tags: { readonly column: 'tags' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly tags: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + readonly tags: { readonly column: 'tags' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.json new file mode 100644 index 0000000000..4b97d815b1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260307T1100_late_branch/start-contract.json @@ -0,0 +1,220 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "comments": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "tags": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "comments": { + "column": "comments" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + }, + "tags": { + "column": "tags" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "comments": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "tags": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/migration.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/migration.ts index d97ea021b9..9d1653e92d 100755 --- a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/migration.ts +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84', - to: 'sha256:2636edd722347d39a60458e4b0733f62e8d316d054fb7cca2b801e91cd619f74', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.d.ts b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.d.ts new file mode 100644 index 0000000000..7e17eb732a --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.d.ts @@ -0,0 +1,302 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly comments: CodecTypes['pg/text@1']['output'] | null; + readonly tags: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly comments: CodecTypes['pg/text@1']['input'] | null; + readonly tags: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly comments: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly tags: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly tags: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + readonly tags: { readonly column: 'tags' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly comments: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly tags: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + readonly avatar: { readonly column: 'avatar' }; + readonly comments: { readonly column: 'comments' }; + readonly tags: { readonly column: 'tags' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.json b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.json new file mode 100644 index 0000000000..4b97d815b1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/long-spine/migrations/app/20260308T1000_add_everything/start-contract.json @@ -0,0 +1,220 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "comments": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "tags": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "comments": { + "column": "comments" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + }, + "tags": { + "column": "tags" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "comments": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "tags": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:99de8c2642999fd9b4c99ea77e53e8e31cb65d66575027eb287e8b0d849e8b84" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1000_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/migration.ts index bdfc9830d1..7836faee7b 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('posts', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1100_add_posts/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/migration.ts index a836c7cb14..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260302T1200_add_avatar/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/migration.ts index 2116e2b66f..ac4493b8c3 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - to: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('bio', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.d.ts new file mode 100644 index 0000000000..eb94201878 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.json new file mode 100644 index 0000000000..f939d86a45 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260303T1000_add_bio/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/migration.ts index bf439f0b7f..988a2c9b19 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:d11106e57024ff51282c4a92f549538eb9b8c77b30c719d5e25e726e9cdb40de', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_a/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/migration.ts index bf439f0b7f..988a2c9b19 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:d11106e57024ff51282c4a92f549538eb9b8c77b30c719d5e25e726e9cdb40de', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_b/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/migration.ts index bf439f0b7f..988a2c9b19 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:d11106e57024ff51282c4a92f549538eb9b8c77b30c719d5e25e726e9cdb40de', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_c/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/migration.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/migration.ts index bf439f0b7f..988a2c9b19 100755 --- a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/migration.ts +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:d11106e57024ff51282c4a92f549538eb9b8c77b30c719d5e25e726e9cdb40de', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.d.ts b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.json b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/multi-branch/migrations/app/20260304T1000_parallel_d/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0719_init/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0719_init/migration.ts index 371132d0b2..e28acfe191 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0719_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0719_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/migration.ts index 67bc88b033..c8735b4c2a 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021', - to: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.d.ts new file mode 100644 index 0000000000..91d56e4b8f --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.d.ts @@ -0,0 +1,167 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.json new file mode 100644 index 0000000000..e603191ef5 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_add_name/start-contract.json @@ -0,0 +1,130 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/migration.ts index 6bdeb4f65d..b359857652 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - to: 'sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.d.ts new file mode 100644 index 0000000000..8c057c8abe --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.d.ts @@ -0,0 +1,182 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.json new file mode 100644 index 0000000000..1c6f99fe0e --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_alice_phone/start-contract.json @@ -0,0 +1,145 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "__unbound__", + "table": "account" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/migration.ts index 7381b9dc3a..7e7ae29c4a 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - to: 'sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.d.ts new file mode 100644 index 0000000000..8c057c8abe --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.d.ts @@ -0,0 +1,182 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.json new file mode 100644 index 0000000000..1c6f99fe0e --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0725_bob_avatar/start-contract.json @@ -0,0 +1,145 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "namespaceId": "__unbound__", + "table": "account" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/migration.ts index 6302096cd3..0f80e37fc5 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0', - to: 'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.d.ts new file mode 100644 index 0000000000..b1a03dc26e --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.d.ts @@ -0,0 +1,206 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.json new file mode 100644 index 0000000000..41fdda41ae --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_bio/start-contract.json @@ -0,0 +1,175 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/migration.ts index 3d2f44a08b..e216fdc764 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1', - to: 'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.d.ts new file mode 100644 index 0000000000..fdbfa917c5 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.d.ts @@ -0,0 +1,218 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.json new file mode 100644 index 0000000000..518ef95e35 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_add_locale/start-contract.json @@ -0,0 +1,190 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/migration.ts index 4ab19d571d..32e106484f 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021', - to: 'sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.d.ts new file mode 100644 index 0000000000..91d56e4b8f --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.d.ts @@ -0,0 +1,167 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.json new file mode 100644 index 0000000000..e603191ef5 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_fast_forward/start-contract.json @@ -0,0 +1,130 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/migration.ts index 2f92a36180..7e7ae29c4a 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026', - to: 'sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.d.ts new file mode 100644 index 0000000000..b0191c4dde --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.d.ts @@ -0,0 +1,194 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.json new file mode 100644 index 0000000000..8c40318198 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_alice/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/migration.ts index 79e5434f77..b359857652 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5', - to: 'sha256:83a1ded0b0045642794c268ef48d21d54bb65a481c13c8b243a7f5821b78d9a0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.d.ts new file mode 100644 index 0000000000..5f2e0fb10c --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.d.ts @@ -0,0 +1,194 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.json new file mode 100644 index 0000000000..9f5b6b84d6 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0726_merge_bob/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/migration.ts index ecd8de7cff..341b9251c9 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, lit, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585', - to: 'sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.d.ts new file mode 100644 index 0000000000..a79809f46f --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.d.ts @@ -0,0 +1,230 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.json new file mode 100644 index 0000000000..038f30c213 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_hotfix/start-contract.json @@ -0,0 +1,205 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/migration.ts index 7114fc2c43..240b50ef04 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026', - to: 'sha256:3bfce91c81146b347dc05f423a71907a82d8b2e78ab5714b2bfab673f673d021', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.d.ts new file mode 100644 index 0000000000..b0191c4dde --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.d.ts @@ -0,0 +1,194 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.json new file mode 100644 index 0000000000..8c40318198 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_alice/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:f5aa17d46d7be94ca3ddf4d14d90e0444291d9c063c52d0e05455726e52e0026" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/migration.ts index a68ed43ffb..7307ad0100 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585', - to: 'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.dropColumn({ schema: '__unbound__', table: 'account', column: 'locale' })]; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.d.ts new file mode 100644 index 0000000000..a79809f46f --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.d.ts @@ -0,0 +1,230 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.json new file mode 100644 index 0000000000..038f30c213 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_locale/start-contract.json @@ -0,0 +1,205 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/migration.ts index dcf022c7ec..e87487a683 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585', - to: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.d.ts new file mode 100644 index 0000000000..a79809f46f --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.d.ts @@ -0,0 +1,230 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.json new file mode 100644 index 0000000000..038f30c213 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0727_rollback_users/start-contract.json @@ -0,0 +1,205 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:bf158ef32daace0a629bfdac5d569b0d43cd81e257e2463aef2545638e2c7585" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/migration.ts index f4abb2d0be..8b63f8a247 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, lit, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5', - to: 'sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.d.ts new file mode 100644 index 0000000000..5f2e0fb10c --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.d.ts @@ -0,0 +1,194 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly avatar: { readonly column: 'avatar' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.json new file mode 100644 index 0000000000..9f5b6b84d6 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0728_promote_bob/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:935a02360e01dda00d62f98429f4347bf765abf9118bca03941383cef87591c5" + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.d.ts new file mode 100644 index 0000000000..440280f543 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.d.ts @@ -0,0 +1,246 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + readonly verified: CodecTypes['pg/bool@1']['output']; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + readonly verified: CodecTypes['pg/bool@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly verified: { + readonly nativeType: 'bool'; + readonly codecId: 'pg/bool@1'; + readonly nullable: false; + readonly default: { + readonly kind: 'literal'; + readonly value: DefaultLiteralValue<'pg/bool@1', true>; + }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly verified: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/bool@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + readonly verified: { readonly column: 'verified' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.json new file mode 100644 index 0000000000..02aff509a2 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0729_reapply_noop/start-contract.json @@ -0,0 +1,224 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "verified": { + "nullable": false, + "type": { + "codecId": "pg/bool@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + }, + "verified": { + "column": "verified" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "verified": { + "codecId": "pg/bool@1", + "default": { + "kind": "literal", + "value": true + }, + "nativeType": "bool", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b" + }, + "target": "postgres", + "targetFamily": "sql" +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/migration.ts index a3f8257392..2e9dc4a327 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:34dd5176d953c101467355b72fd2adf3e49c97bf13d8198dcc0dafec4d6341ce', - to: 'sha256:f1e8cde6ed1bd4ed5851d1308cd58431169236230778c42382189cdc0557a7fb', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.d.ts new file mode 100644 index 0000000000..847a0a0877 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.d.ts @@ -0,0 +1,299 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:34dd5176d953c101467355b72fd2adf3e49c97bf13d8198dcc0dafec4d6341ce'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:07bf8bd20b4ab4505cc0a5908a26eb3426e28c15e4876cf38ab0918b9820c405'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + readonly verified: CodecTypes['pg/bool@1']['output']; + }; + readonly widget: { readonly id: Char<36>; readonly value: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + readonly verified: CodecTypes['pg/bool@1']['input']; + }; + readonly widget: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly value: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly verified: { + readonly nativeType: 'bool'; + readonly codecId: 'pg/bool@1'; + readonly nullable: false; + readonly default: { + readonly kind: 'literal'; + readonly value: DefaultLiteralValue<'pg/bool@1', true>; + }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly widget: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly value: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly verified: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/bool@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + readonly verified: { readonly column: 'verified' }; + }; + }; + }; + readonly widget: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly value: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'widget'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly value: { readonly column: 'value' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + readonly widget: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'widget' }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + { + readonly ref: { readonly table: 'widget'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.json new file mode 100644 index 0000000000..3fb182214a --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_experiment/start-contract.json @@ -0,0 +1,297 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:07bf8bd20b4ab4505cc0a5908a26eb3426e28c15e4876cf38ab0918b9820c405", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + }, + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "widget" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + }, + "widget": { + "model": "widget", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "verified": { + "codecId": "pg/bool@1", + "default": { + "kind": "literal", + "value": true + }, + "nativeType": "bool", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + }, + "widget": { + "columns": { + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "value": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:34dd5176d953c101467355b72fd2adf3e49c97bf13d8198dcc0dafec4d6341ce" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "verified": { + "nullable": false, + "type": { + "codecId": "pg/bool@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + }, + "verified": { + "column": "verified" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + }, + "widget": { + "fields": { + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "value": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "id": { + "column": "id" + }, + "value": { + "column": "value" + } + }, + "table": "widget", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/migration.ts index 3d13096667..5c73b1c846 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:f1e8cde6ed1bd4ed5851d1308cd58431169236230778c42382189cdc0557a7fb', - to: 'sha256:34dd5176d953c101467355b72fd2adf3e49c97bf13d8198dcc0dafec4d6341ce', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.dropColumn({ schema: '__unbound__', table: 'widget', column: 'count' })]; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.d.ts new file mode 100644 index 0000000000..5b1c04b373 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.d.ts @@ -0,0 +1,314 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f1e8cde6ed1bd4ed5851d1308cd58431169236230778c42382189cdc0557a7fb'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:07bf8bd20b4ab4505cc0a5908a26eb3426e28c15e4876cf38ab0918b9820c405'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + readonly verified: CodecTypes['pg/bool@1']['output']; + }; + readonly widget: { + readonly id: Char<36>; + readonly value: CodecTypes['pg/text@1']['output']; + readonly count: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + readonly verified: CodecTypes['pg/bool@1']['input']; + }; + readonly widget: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly value: CodecTypes['pg/text@1']['input']; + readonly count: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly verified: { + readonly nativeType: 'bool'; + readonly codecId: 'pg/bool@1'; + readonly nullable: false; + readonly default: { + readonly kind: 'literal'; + readonly value: DefaultLiteralValue<'pg/bool@1', true>; + }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + readonly widget: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly value: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly count: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly verified: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/bool@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + readonly verified: { readonly column: 'verified' }; + }; + }; + }; + readonly widget: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly value: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly count: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'widget'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly value: { readonly column: 'value' }; + readonly count: { readonly column: 'count' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + readonly widget: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'widget' }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + { + readonly ref: { readonly table: 'widget'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.json new file mode 100644 index 0000000000..4f0ac1e88d --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260601T0730_revert_experiment/start-contract.json @@ -0,0 +1,312 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + }, + "widget": { + "model": "widget", + "namespace": "__unbound__" + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "verified": { + "codecId": "pg/bool@1", + "default": { + "kind": "literal", + "value": true + }, + "nativeType": "bool", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + }, + "widget": { + "columns": { + "count": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "value": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:f1e8cde6ed1bd4ed5851d1308cd58431169236230778c42382189cdc0557a7fb" + }, + "execution": { + "executionHash": "sha256:07bf8bd20b4ab4505cc0a5908a26eb3426e28c15e4876cf38ab0918b9820c405", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + }, + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "widget" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "verified": { + "nullable": false, + "type": { + "codecId": "pg/bool@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + }, + "verified": { + "column": "verified" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + }, + "widget": { + "fields": { + "count": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "value": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "count": { + "column": "count" + }, + "id": { + "column": "id" + }, + "value": { + "column": "value" + } + }, + "table": "widget", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/migration.ts index 1b3fb00f9e..13267fbda9 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b', - to: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.d.ts new file mode 100644 index 0000000000..440280f543 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.d.ts @@ -0,0 +1,246 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly locale: CodecTypes['pg/text@1']['output'] | null; + readonly verified: CodecTypes['pg/bool@1']['output']; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly locale: CodecTypes['pg/text@1']['input'] | null; + readonly verified: CodecTypes['pg/bool@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly locale: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly verified: { + readonly nativeType: 'bool'; + readonly codecId: 'pg/bool@1'; + readonly nullable: false; + readonly default: { + readonly kind: 'literal'; + readonly value: DefaultLiteralValue<'pg/bool@1', true>; + }; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly locale: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly verified: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/bool@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + readonly locale: { readonly column: 'locale' }; + readonly verified: { readonly column: 'verified' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.json new file mode 100644 index 0000000000..02aff509a2 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1624_rollback_to_users_from_tip/start-contract.json @@ -0,0 +1,224 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "locale": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "verified": { + "nullable": false, + "type": { + "codecId": "pg/bool@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "locale": { + "column": "locale" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + }, + "verified": { + "column": "verified" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "locale": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "verified": { + "codecId": "pg/bool@1", + "default": { + "kind": "literal", + "value": true + }, + "nativeType": "bool", + "nullable": false + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:f66098408da51786d8c6701a2b10db2e90f4b7e138eb5e95f84dc61e156d242b" + }, + "target": "postgres", + "targetFamily": "sql" +} diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/migration.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/migration.ts index 90766320a6..bce72e8f16 100755 --- a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/migration.ts +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1', - to: 'sha256:419c09911c25cf9b97e60ee157c61a126accfa5f26f5cdb7954667c704f53753', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.d.ts b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.d.ts new file mode 100644 index 0000000000..fdbfa917c5 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.d.ts @@ -0,0 +1,218 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12'>; +export type ProfileHash = + ProfileHashBase<'sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly account: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly name: CodecTypes['pg/text@1']['output'] | null; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly avatar: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly account: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly name: CodecTypes['pg/text@1']['input'] | null; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly avatar: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly account: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly name: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly avatar: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly account: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly name: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly avatar: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'account'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly name: { readonly column: 'name' }; + readonly phone: { readonly column: 'phone' }; + readonly avatar: { readonly column: 'avatar' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly account: { + readonly namespace: '__unbound__' & NamespaceId; + readonly model: 'account'; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'account'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = Contract['models']; diff --git a/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.json b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.json new file mode 100644 index 0000000000..518ef95e35 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/showcase/migrations/app/20260602T1626_rollback_to_users_from_bio/start-contract.json @@ -0,0 +1,190 @@ +{ + "_generated": { + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit", + "warning": "⚠️ GENERATED FILE - DO NOT EDIT" + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "execution": { + "executionHash": "sha256:726228fb3c6f3d975bc1d2c5847712e21289cc05635b1155e177fab93ca55b12", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "account" + } + } + ] + } + }, + "extensionPacks": {}, + "meta": {}, + "profileHash": "sha256:1a8dbe044289f30a1de958fe800cc5a8378b285d2e126a8c44b58864bac2c18e", + "roots": { + "account": { + "model": "account", + "namespace": "__unbound__" + } + }, + "schemaVersion": "1", + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "account": { + "columns": { + "avatar": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "name": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:3705eb1cd04a52180d1206181446bb87e18bb32afcc3d1dacbec685ca2d449d1" + }, + "target": "postgres", + "targetFamily": "sql", + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "account": { + "fields": { + "avatar": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "name": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "avatar": { + "column": "avatar" + }, + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "name": { + "column": "name" + }, + "phone": { + "column": "phone" + } + }, + "table": "account", + "namespaceId": "__unbound__" + } + } + } + } + } + } +} diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260302T1000_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/migration.ts index 2116e2b66f..ac4493b8c3 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - to: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('bio', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.d.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.d.ts new file mode 100644 index 0000000000..eb94201878 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.d.ts @@ -0,0 +1,217 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.json b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.json new file mode 100644 index 0000000000..f939d86a45 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260303T1000_add_bio/start-contract.json @@ -0,0 +1,145 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/migration.ts index 0537d3aded..7836faee7b 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('posts', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.json b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260304T1000_add_posts/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/migration.ts index c8597d9a85..15ed135c2f 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.d.ts new file mode 100644 index 0000000000..c5da3fe1df --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.d.ts @@ -0,0 +1,251 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + readonly posts: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + readonly posts: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly posts: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly posts: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + readonly posts: { readonly column: 'posts' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.json b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.json new file mode 100644 index 0000000000..af3a3d49a4 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260305T1000_rollback_to_phone/start-contract.json @@ -0,0 +1,175 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "posts": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + }, + "posts": { + "column": "posts" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "posts": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:7e951c7a95f42ca0420d9c4aae3602e32911606399982347621943fce34076d8" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/migration.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/migration.ts index f27e9d7cf8..33d8e0460a 100755 --- a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078', - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.d.ts b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.d.ts new file mode 100644 index 0000000000..b283855412 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.d.ts @@ -0,0 +1,234 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { + readonly id: Char<36>; + readonly email: CodecTypes['pg/text@1']['output']; + readonly phone: CodecTypes['pg/text@1']['output'] | null; + readonly bio: CodecTypes['pg/text@1']['output'] | null; + }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + readonly phone: CodecTypes['pg/text@1']['input'] | null; + readonly bio: CodecTypes['pg/text@1']['input'] | null; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + readonly phone: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + readonly bio: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: true; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly phone: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + readonly bio: { + readonly nullable: true; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + readonly phone: { readonly column: 'phone' }; + readonly bio: { readonly column: 'bio' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.json b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.json new file mode 100644 index 0000000000..3791fec5e8 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/skip-rollback/migrations/app/20260306T1000_rollback_to_init/start-contract.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "bio": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + }, + "phone": { + "nullable": true, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + } + }, + "relations": {}, + "storage": { + "fields": { + "bio": { + "column": "bio" + }, + "email": { + "column": "email" + }, + "id": { + "column": "id" + }, + "phone": { + "column": "phone" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "bio": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + }, + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + }, + "phone": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": true + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:827997ce6f33b7193b0a9eda969affe757e0d54209fde344afcd4d41321c5078" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260301T1000_init/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260301T1000_init/migration.ts index aa8b2c8cc6..f2c1928489 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260301T1000_init/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260301T1000_init/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/migration.ts index dd0b08f99c..f1e0d46dc5 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:93be6c200743261baf55f0586b1380a1c0ade3c48730c09a8fec71ba419c2464', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('phone', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.d.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.json b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1000_add_phone/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/migration.ts index bdfc9830d1..7836faee7b 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:afdcd8ee600252054660e79266ede99d1f5227b586225985565eff24414696d0', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [this.addColumn({ schema: '__unbound__', table: 'user', column: col('posts', 'text') })]; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.d.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.json b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1100_add_posts/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/migration.ts index a836c7cb14..30d4285cae 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:7e3fa7fbe98974385451444c229337c87ec90c547a9214f81700f86b5af3563d', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.d.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.json b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1200_add_avatar/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/migration.ts index becbc15179..46986aa325 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:2796854eb60b7ce66a0cd34d550680931f82fc2d2dca048726f971d1cc3aef10', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.d.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.json b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1300_add_category/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/migration.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/migration.ts index e96045e43e..a6943071b8 100755 --- a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/migration.ts +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/migration.ts @@ -1,13 +1,13 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI } from '@prisma-next/postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: 'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4', - to: 'sha256:f224748df616e79a307d7d1d964b2cca74f858a8f00c95ed131d7675a3e74554', - }; - } +export default class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.d.ts b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.d.ts new file mode 100644 index 0000000000..39cc24d885 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.d.ts @@ -0,0 +1,197 @@ +// ⚠️ GENERATED FILE - DO NOT EDIT +// This file is automatically generated by 'prisma-next contract emit'. +// To regenerate, run: prisma-next contract emit +import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types'; +import type { + Bit, + Char, + CodecTypes as PgTypes, + Interval, + JsonValue, + Numeric, + Time, + Timestamp, + Timestamptz, + Timetz, + VarBit, + Varchar, +} from '@prisma-next/target-postgres/codec-types'; + +import type { + ContractWithTypeMaps, + TypeMaps as TypeMapsType, +} from '@prisma-next/sql-contract/types'; +import type { + Contract as ContractType, + ContractModelsMap, + ExecutionHashBase, + NamespaceId, + ProfileHashBase, + StorageHashBase, +} from '@prisma-next/contract/types'; + +export type StorageHash = + StorageHashBase<'sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4'>; +export type ExecutionHash = + ExecutionHashBase<'sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545'>; +export type ProfileHash = + ProfileHashBase<'sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb'>; + +export type CodecTypes = PgTypes; +export type LaneCodecTypes = CodecTypes; +export type QueryOperationTypes = PgAdapterQueryOps; +type DefaultLiteralValue = CodecId extends keyof CodecTypes + ? CodecTypes[CodecId]['output'] + : _Encoded; + +export type FieldOutputTypes = { + readonly user: { readonly id: Char<36>; readonly email: CodecTypes['pg/text@1']['output'] }; +}; +export type FieldInputTypes = { + readonly user: { + readonly id: CodecTypes['sql/char@1']['input']; + readonly email: CodecTypes['pg/text@1']['input']; + }; +}; +export type TypeMaps = TypeMapsType< + CodecTypes, + QueryOperationTypes, + FieldOutputTypes, + FieldInputTypes +>; + +type ContractBase = Omit< + ContractType< + { + readonly namespaces: { + readonly __unbound__: { + readonly id: '__unbound__'; + readonly kind: 'postgres-schema'; + readonly tables: { + readonly user: { + columns: { + readonly id: { + readonly nativeType: 'character'; + readonly codecId: 'sql/char@1'; + readonly nullable: false; + readonly typeParams: { readonly length: 36 }; + }; + readonly email: { + readonly nativeType: 'text'; + readonly codecId: 'pg/text@1'; + readonly nullable: false; + }; + }; + primaryKey: { readonly columns: readonly ['id'] }; + uniques: readonly []; + indexes: readonly []; + foreignKeys: readonly []; + }; + }; + }; + }; + readonly storageHash: StorageHash; + }, + { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + } + >, + 'roots' | 'domain' +> & { + readonly target: 'postgres'; + readonly targetFamily: 'sql'; + readonly roots: { + readonly user: { readonly namespace: '__unbound__' & NamespaceId; readonly model: 'user' }; + }; + readonly domain: { + readonly namespaces: { + readonly __unbound__: { + readonly models: { + readonly user: { + readonly fields: { + readonly id: { + readonly nullable: false; + readonly type: { + readonly kind: 'scalar'; + readonly codecId: 'sql/char@1'; + readonly typeParams: { readonly length: 36 }; + }; + }; + readonly email: { + readonly nullable: false; + readonly type: { readonly kind: 'scalar'; readonly codecId: 'pg/text@1' }; + }; + }; + readonly relations: Record; + readonly storage: { + readonly table: 'user'; + readonly fields: { + readonly id: { readonly column: 'id' }; + readonly email: { readonly column: 'email' }; + }; + }; + }; + }; + }; + }; + }; + readonly capabilities: { + readonly postgres: { + readonly distinctOn: true; + readonly jsonAgg: true; + readonly lateral: true; + readonly limit: true; + readonly orderBy: true; + readonly returning: true; + }; + readonly sql: { + readonly defaultInInsert: true; + readonly enums: true; + readonly lateral: true; + readonly returning: true; + }; + }; + readonly extensionPacks: {}; + readonly execution: { + readonly executionHash: ExecutionHash; + readonly mutations: { + readonly defaults: readonly [ + { + readonly ref: { readonly table: 'user'; readonly column: 'id' }; + readonly onCreate: { readonly kind: 'generator'; readonly id: 'uuidv4' }; + }, + ]; + }; + }; + readonly meta: {}; + + readonly profileHash: ProfileHash; +}; + +export type Contract = ContractWithTypeMaps; + +export type Namespaces = Contract['storage']['namespaces']; +export type Models = ContractModelsMap; diff --git a/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.json b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.json new file mode 100644 index 0000000000..8c8c387ec1 --- /dev/null +++ b/examples/prisma-next-demo/fixtures/wide-fan/migrations/app/20260302T1400_add_settings/start-contract.json @@ -0,0 +1,130 @@ +{ + "schemaVersion": "1", + "targetFamily": "sql", + "target": "postgres", + "profileHash": "sha256:9c8aa3114e84ed3b7ea2bd57526d9c2e1bf7c5292be694e9d3801f566fda7ccb", + "roots": { + "user": { + "model": "user", + "namespace": "__unbound__" + } + }, + "domain": { + "namespaces": { + "__unbound__": { + "models": { + "user": { + "fields": { + "email": { + "nullable": false, + "type": { + "codecId": "pg/text@1", + "kind": "scalar" + } + }, + "id": { + "nullable": false, + "type": { + "codecId": "sql/char@1", + "kind": "scalar", + "typeParams": { + "length": 36 + } + } + } + }, + "relations": {}, + "storage": { + "fields": { + "email": { + "column": "email" + }, + "id": { + "column": "id" + } + }, + "table": "user", + "namespaceId": "__unbound__" + } + } + } + } + } + }, + "storage": { + "namespaces": { + "__unbound__": { + "id": "__unbound__", + "kind": "postgres-unbound-schema", + "tables": { + "user": { + "columns": { + "email": { + "codecId": "pg/text@1", + "nativeType": "text", + "nullable": false + }, + "id": { + "codecId": "sql/char@1", + "nativeType": "character", + "nullable": false, + "typeParams": { + "length": 36 + } + } + }, + "foreignKeys": [], + "indexes": [], + "primaryKey": { + "columns": [ + "id" + ] + }, + "uniques": [] + } + } + } + }, + "storageHash": "sha256:789dd79ab5ab725be1b6ced088109b803a4d62f9874f932eb384a868d94360a4" + }, + "execution": { + "executionHash": "sha256:5230f959b26fc69f483bc89f66df7c95e0e93400e0f4cb14311e4cbf963cd545", + "mutations": { + "defaults": [ + { + "onCreate": { + "id": "uuidv4", + "kind": "generator" + }, + "ref": { + "column": "id", + "table": "user" + } + } + ] + } + }, + "capabilities": { + "postgres": { + "distinctOn": true, + "jsonAgg": true, + "lateral": true, + "limit": true, + "orderBy": true, + "returning": true + }, + "sql": { + "defaultInInsert": true, + "enums": true, + "lateral": true, + "returning": true + } + }, + "extensionPacks": {}, + "meta": {}, + "_generated": { + "warning": "⚠️ GENERATED FILE - DO NOT EDIT", + "message": "This file is automatically generated by \"prisma-next contract emit\".", + "regenerate": "To regenerate, run: prisma-next contract emit" + } +} diff --git a/examples/prisma-next-demo/migrations/app/20260422T0720_initial/migration.ts b/examples/prisma-next-demo/migrations/app/20260422T0720_initial/migration.ts index 2e64f87ccb..71e2a936e1 100644 --- a/examples/prisma-next-demo/migrations/app/20260422T0720_initial/migration.ts +++ b/examples/prisma-next-demo/migrations/app/20260422T0720_initial/migration.ts @@ -9,14 +9,11 @@ import { rawSql, unique, } from '@prisma-next/target-postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:9f49f8f9e51a9cc016f1ec2098ebae9406521a3cc2cf00207adc795078333d8b', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/prisma-next-postgis-demo/migrations/app/20260512T1309_migration/migration.ts b/examples/prisma-next-postgis-demo/migrations/app/20260512T1309_migration/migration.ts index 3074addc68..0e81ab7627 100755 --- a/examples/prisma-next-postgis-demo/migrations/app/20260512T1309_migration/migration.ts +++ b/examples/prisma-next-postgis-demo/migrations/app/20260512T1309_migration/migration.ts @@ -1,13 +1,10 @@ #!/usr/bin/env -S node import { col, Migration, MigrationCLI, primaryKey } from '@prisma-next/target-postgres/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; -export default class M extends Migration { - override describe() { - return { - from: null, - to: 'sha256:6c2de2dd04e4425aa3a8ba8e05df0c812204ef610bb975ae1b2b7d19c74fbdb2', - }; - } +export default class M extends Migration { + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts index 7fddba6c19..c818fc4271 100755 --- a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts @@ -5,53 +5,296 @@ import { createCollection, createIndex } from '@prisma-next/target-mongo/migrati import type { Contract as End } from './end-contract'; import endContractJson from './end-contract.json' with { type: 'json' }; -class M extends Migration { +class M extends Migration { override readonly endContractJson = endContractJson; override get operations() { - const COLLECTIONS = this.endContract.collection; - const CARTS_VALIDATOR = COLLECTIONS.carts.validator; - const EVENTS_VALIDATOR = COLLECTIONS.events.validator; - const INVOICES_VALIDATOR = COLLECTIONS.invoices.validator; - const LOCATIONS_VALIDATOR = COLLECTIONS.locations.validator; - const ORDERS_VALIDATOR = COLLECTIONS.orders.validator; - const PRODUCTS_VALIDATOR = COLLECTIONS.products.validator; - const USERS_VALIDATOR = COLLECTIONS.users.validator; return [ createCollection('carts', { - validator: { $jsonSchema: CARTS_VALIDATOR.jsonSchema }, - validationLevel: CARTS_VALIDATOR.validationLevel, - validationAction: CARTS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + items: { + bsonType: 'array', + items: { + additionalProperties: false, + bsonType: 'object', + properties: { + amount: { bsonType: 'int' }, + brand: { bsonType: 'string' }, + image: { + additionalProperties: false, + bsonType: 'object', + properties: { url: { bsonType: 'string' } }, + required: ['url'], + }, + name: { bsonType: 'string' }, + price: { + additionalProperties: false, + bsonType: 'object', + properties: { + amount: { bsonType: 'double' }, + currency: { bsonType: 'string' }, + }, + required: ['amount', 'currency'], + }, + productId: { bsonType: 'string' }, + }, + required: ['amount', 'brand', 'image', 'name', 'price', 'productId'], + }, + }, + userId: { bsonType: 'objectId' }, + }, + required: ['_id', 'items', 'userId'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('events', { - validator: { $jsonSchema: EVENTS_VALIDATOR.jsonSchema }, - validationLevel: EVENTS_VALIDATOR.validationLevel, - validationAction: EVENTS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + bsonType: 'object', + oneOf: [ + { + additionalProperties: false, + properties: { + _id: { bsonType: 'objectId' }, + brand: { bsonType: 'string' }, + exitMethod: { bsonType: ['null', 'string'] }, + productId: { bsonType: 'string' }, + sessionId: { bsonType: 'string' }, + subCategory: { bsonType: 'string' }, + timestamp: { bsonType: 'date' }, + type: { enum: ['view-product'] }, + userId: { bsonType: 'string' }, + }, + required: ['brand', 'productId', 'subCategory', 'type'], + }, + { + additionalProperties: false, + properties: { + _id: { bsonType: 'objectId' }, + query: { bsonType: 'string' }, + sessionId: { bsonType: 'string' }, + timestamp: { bsonType: 'date' }, + type: { enum: ['search'] }, + userId: { bsonType: 'string' }, + }, + required: ['query', 'type'], + }, + { + additionalProperties: false, + properties: { + _id: { bsonType: 'objectId' }, + brand: { bsonType: 'string' }, + productId: { bsonType: 'string' }, + sessionId: { bsonType: 'string' }, + timestamp: { bsonType: 'date' }, + type: { enum: ['add-to-cart'] }, + userId: { bsonType: 'string' }, + }, + required: ['brand', 'productId', 'type'], + }, + ], + properties: { + _id: { bsonType: 'objectId' }, + sessionId: { bsonType: 'string' }, + timestamp: { bsonType: 'date' }, + type: { bsonType: 'string' }, + userId: { bsonType: 'string' }, + }, + required: ['_id', 'sessionId', 'timestamp', 'type', 'userId'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('invoices', { - validator: { $jsonSchema: INVOICES_VALIDATOR.jsonSchema }, - validationLevel: INVOICES_VALIDATOR.validationLevel, - validationAction: INVOICES_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + issuedAt: { bsonType: 'date' }, + items: { + bsonType: 'array', + items: { + additionalProperties: false, + bsonType: 'object', + properties: { + amount: { bsonType: 'int' }, + lineTotal: { bsonType: 'double' }, + name: { bsonType: 'string' }, + unitPrice: { bsonType: 'double' }, + }, + required: ['amount', 'lineTotal', 'name', 'unitPrice'], + }, + }, + orderId: { bsonType: 'objectId' }, + subtotal: { bsonType: 'double' }, + tax: { bsonType: 'double' }, + total: { bsonType: 'double' }, + }, + required: ['_id', 'issuedAt', 'items', 'orderId', 'subtotal', 'tax', 'total'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('locations', { - validator: { $jsonSchema: LOCATIONS_VALIDATOR.jsonSchema }, - validationLevel: LOCATIONS_VALIDATOR.validationLevel, - validationAction: LOCATIONS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + city: { bsonType: 'string' }, + country: { bsonType: 'string' }, + name: { bsonType: 'string' }, + postalCode: { bsonType: 'string' }, + streetAndNumber: { bsonType: 'string' }, + }, + required: ['_id', 'city', 'country', 'name', 'postalCode', 'streetAndNumber'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('orders', { - validator: { $jsonSchema: ORDERS_VALIDATOR.jsonSchema }, - validationLevel: ORDERS_VALIDATOR.validationLevel, - validationAction: ORDERS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + items: { + bsonType: 'array', + items: { + additionalProperties: false, + bsonType: 'object', + properties: { + amount: { bsonType: 'int' }, + brand: { bsonType: 'string' }, + image: { + additionalProperties: false, + bsonType: 'object', + properties: { url: { bsonType: 'string' } }, + required: ['url'], + }, + name: { bsonType: 'string' }, + price: { + additionalProperties: false, + bsonType: 'object', + properties: { + amount: { bsonType: 'double' }, + currency: { bsonType: 'string' }, + }, + required: ['amount', 'currency'], + }, + productId: { bsonType: 'string' }, + }, + required: ['amount', 'brand', 'image', 'name', 'price', 'productId'], + }, + }, + shippingAddress: { bsonType: 'string' }, + statusHistory: { + bsonType: 'array', + items: { + additionalProperties: false, + bsonType: 'object', + properties: { status: { bsonType: 'string' }, timestamp: { bsonType: 'date' } }, + required: ['status', 'timestamp'], + }, + }, + type: { bsonType: 'string' }, + userId: { bsonType: 'objectId' }, + }, + required: ['_id', 'items', 'shippingAddress', 'statusHistory', 'type', 'userId'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('products', { - validator: { $jsonSchema: PRODUCTS_VALIDATOR.jsonSchema }, - validationLevel: PRODUCTS_VALIDATOR.validationLevel, - validationAction: PRODUCTS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + articleType: { bsonType: 'string' }, + brand: { bsonType: 'string' }, + code: { bsonType: 'string' }, + description: { bsonType: 'string' }, + image: { + additionalProperties: false, + bsonType: 'object', + properties: { url: { bsonType: 'string' } }, + required: ['url'], + }, + name: { bsonType: 'string' }, + price: { + additionalProperties: false, + bsonType: 'object', + properties: { amount: { bsonType: 'double' }, currency: { bsonType: 'string' } }, + required: ['amount', 'currency'], + }, + primaryCategory: { bsonType: 'string' }, + subCategory: { bsonType: 'string' }, + }, + required: [ + '_id', + 'articleType', + 'brand', + 'code', + 'description', + 'image', + 'name', + 'price', + 'primaryCategory', + 'subCategory', + ], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createCollection('users', { - validator: { $jsonSchema: USERS_VALIDATOR.jsonSchema }, - validationLevel: USERS_VALIDATOR.validationLevel, - validationAction: USERS_VALIDATOR.validationAction, + validator: { + $jsonSchema: { + additionalProperties: false, + bsonType: 'object', + properties: { + _id: { bsonType: 'objectId' }, + address: { + oneOf: [ + { bsonType: 'null' }, + { + additionalProperties: false, + bsonType: 'object', + properties: { + city: { bsonType: 'string' }, + country: { bsonType: 'string' }, + postalCode: { bsonType: 'string' }, + streetAndNumber: { bsonType: 'string' }, + }, + required: ['city', 'country', 'postalCode', 'streetAndNumber'], + }, + ], + }, + email: { bsonType: 'string' }, + name: { bsonType: 'string' }, + }, + required: ['_id', 'email', 'name'], + }, + }, + validationLevel: 'strict', + validationAction: 'error', }), createIndex('carts', [{ direction: 1, field: 'userId' }], { unique: true }), createIndex( diff --git a/examples/retail-store/migrations/app/20260513T0507_add_product_category_index/migration.ts b/examples/retail-store/migrations/app/20260513T0507_add_product_category_index/migration.ts index c9733c93e3..0c9b5f1f73 100755 --- a/examples/retail-store/migrations/app/20260513T0507_add_product_category_index/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0507_add_product_category_index/migration.ts @@ -2,14 +2,14 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createIndex } from '@prisma-next/target-mongo/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -class M extends Migration { - override describe() { - return { - from: 'sha256:8ee1e7ce30ed334572583d826d9c41388c46f7db82ae2352c3a3fccf1de7cbab', - to: 'sha256:059f3f35403c5a7a90851c23f1028e16d5250630f8a82fba33053e9a50534589', - }; - } +class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/scripts/codemod-migration-shape.mjs b/scripts/codemod-migration-shape.mjs new file mode 100644 index 0000000000..0dad69c110 --- /dev/null +++ b/scripts/codemod-migration-shape.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * One-shot codemod: convert a committed example `migration.ts` from the old + * hand-written `describe()` scaffold to the new contract-JSON scaffold that the + * migration generator (`renderCallsToTypeScript`, TML-2892) now emits. + * + * The transform is source-to-source and behavior-preserving: + * - The `operations` getter body is kept verbatim (so `ops.json` is + * byte-identical after the migration re-emits). + * - `describe()` is removed; its `from`/`to` only decide the baseline shape. + * The base derives describe() from the imported contract JSON, whose + * `storage.storageHash` already equals the committed from/to hashes. + * - Contract-JSON imports + `endContractJson` / `startContractJson` fields are + * injected; the class header gets `` (or `` baseline). + * + * Idempotent: a migration already on the new shape (`endContractJson` field, no + * `describe()`) is left untouched. + * + * Final formatting is NOT done here — the caller runs `biome check --write` so + * the output matches the generator's biome-formatted shape exactly. + * + * Usage: node scripts/codemod-migration-shape.mjs [ ...] + */ + +import { readFileSync, writeFileSync } from 'node:fs'; + +/** Extract the `from:`/`to:` from a `describe()` return literal. */ +function parseDescribe(src) { + // Match `override describe() { return { from: , to: ... }; }` + const fromMatch = src.match(/\bfrom:\s*(null|['"]sha256:[0-9a-f]+['"])/); + const toMatch = src.match(/\bto:\s*['"](sha256:[0-9a-f]+)['"]/); + if (!toMatch) { + throw new Error('codemod: no `to: "sha256:..."` found in describe()'); + } + const isBaseline = !fromMatch || fromMatch[1] === 'null'; + return { isBaseline }; +} + +/** Remove the whole `override describe() { ... }` method (brace-balanced). */ +function stripDescribe(src) { + const start = src.search(/\n[ \t]*override describe\(\)\s*\{/); + if (start === -1) return src; + // find the method's opening brace, then balance. + let i = src.indexOf('{', start); + let depth = 0; + for (; i < src.length; i++) { + if (src[i] === '{') depth++; + else if (src[i] === '}') { + depth--; + if (depth === 0) break; + } + } + // i is at the closing brace of the method. Drop from `start` (the leading + // newline) through the closing brace and any trailing blank line. + let end = i + 1; + if (src.slice(end).startsWith('\n\n')) end += 1; // collapse one blank line + return src.slice(0, start) + src.slice(end); +} + +function transform(path) { + let src = readFileSync(path, 'utf8'); + + if (src.includes('endContractJson =') && !/\boverride describe\(\)/.test(src)) { + return { path, changed: false, reason: 'already new shape' }; + } + if (!/\boverride describe\(\)/.test(src)) { + return { path, changed: false, reason: 'no describe() to convert (skipped)' }; + } + + const { isBaseline } = parseDescribe(src); + + // 1. Strip describe(). + src = stripDescribe(src); + + // 2. Inject contract-JSON imports after the shebang (biome re-sorts later). + const contractImports = [ + `import type { Contract as End } from './end-contract';`, + `import endContract from './end-contract.json' with { type: 'json' };`, + ...(isBaseline + ? [] + : [ + `import type { Contract as Start } from './start-contract';`, + `import startContract from './start-contract.json' with { type: 'json' };`, + ]), + ].join('\n'); + + const shebang = '#!/usr/bin/env -S node\n'; + if (src.startsWith(shebang)) { + src = shebang + contractImports + '\n' + src.slice(shebang.length); + } else { + src = contractImports + '\n' + src; + } + + // 3. Add the `` / `` generic to the class header and + // inject the field(s) right after the opening brace. + const generics = isBaseline ? '' : ''; + const fields = isBaseline + ? ' override readonly endContractJson = endContract;\n\n' + : ' override readonly startContractJson = startContract;\n override readonly endContractJson = endContract;\n\n'; + + const classRe = /((?:export default )?class \w+ extends Migration)(\s*\{)/; + if (!classRe.test(src)) { + throw new Error('codemod: no `class … extends Migration {` header found'); + } + src = src.replace( + classRe, + (_m, head, brace) => `${head}${generics}${brace.replace(/\s*\{/, ' {')}\n${fields}`, + ); + + writeFileSync(path, src, 'utf8'); + return { path, changed: true, isBaseline }; +} + +function main() { + const paths = process.argv.slice(2); + if (paths.length === 0) { + console.error('usage: node scripts/codemod-migration-shape.mjs ...'); + process.exit(2); + } + let changed = 0; + for (const p of paths) { + const r = transform(p); + if (r.changed) { + changed++; + console.log( + `codemod: rewrote ${p} (${r.isBaseline ? 'baseline ' : 'with-start '})`, + ); + } else { + console.log(`codemod: skipped ${p} (${r.reason})`); + } + } + console.log(`codemod: ${changed}/${paths.length} migration(s) rewritten`); +} + +main(); diff --git a/test/integration/test/cli-journeys/mongo-migration.e2e.test.ts b/test/integration/test/cli-journeys/mongo-migration.e2e.test.ts index 618732bf54..02d4f77e8a 100644 --- a/test/integration/test/cli-journeys/mongo-migration.e2e.test.ts +++ b/test/integration/test/cli-journeys/mongo-migration.e2e.test.ts @@ -292,8 +292,12 @@ describe('Journey: Mongo migration authoring (offline)', { timeout: timeouts.spi expect(migrationTs).toContain( "import { Migration } from '@prisma-next/family-mongo/migration'", ); - expect(migrationTs).toContain('class M extends Migration'); - expect(migrationTs).toContain('describe()'); + // New generator shape: the base derives describe() from the imported contract + // JSON, so the scaffold carries `Migration<…, End>` + the endContractJson + // field and emits no describe() / hash literals. + expect(migrationTs).toContain('class M extends Migration<'); + expect(migrationTs).not.toContain('describe()'); + expect(migrationTs).toContain('override readonly endContractJson = endContract;'); expect(migrationTs).toContain('get operations()'); expect(migrationTs).toContain('return ['); // Empty stub: factory imports omitted because no calls were rendered. From b4331580eadf5a7f4a8ad0ef177dc9897a58f04f Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 1 Jul 2026 14:48:13 +0200 Subject: [PATCH 10/12] feat(tml-2892): regenerate #880's new migration + align retail-store 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../app/20260513T0505_initial/migration.ts | 4 ++-- .../migration.ts | 8 ++++---- .../migration.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts index c818fc4271..c0c198068d 100755 --- a/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0505_initial/migration.ts @@ -3,10 +3,10 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { createCollection, createIndex } from '@prisma-next/target-mongo/migration'; import type { Contract as End } from './end-contract'; -import endContractJson from './end-contract.json' with { type: 'json' }; +import endContract from './end-contract.json' with { type: 'json' }; class M extends Migration { - override readonly endContractJson = endContractJson; + override readonly endContractJson = endContract; override get operations() { return [ diff --git a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts index 40f0de9659..8541c81c5e 100755 --- a/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts +++ b/examples/retail-store/migrations/app/20260513T0508_backfill_product_status/migration.ts @@ -11,9 +11,9 @@ import { } from '@prisma-next/mongo-query-ast/execution'; import { dataTransform, setValidation } from '@prisma-next/target-mongo/migration'; import type { Contract as End } from './end-contract'; -import endContractJson from './end-contract.json' with { type: 'json' }; +import endContract from './end-contract.json' with { type: 'json' }; import type { Contract as Start } from './start-contract'; -import startContractJson from './start-contract.json' with { type: 'json' }; +import startContract from './start-contract.json' with { type: 'json' }; function existingProductsWithoutStatus(storageHash: string): MongoQueryPlan { return { @@ -44,8 +44,8 @@ function backfillRun(storageHash: string): MongoQueryPlan { } class BackfillProductStatus extends Migration { - override readonly startContractJson = startContractJson; - override readonly endContractJson = endContractJson; + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; // `migration new` records the contract delta as `from` → `to` but produces an // empty `operations` array; the author is responsible for declaring the work diff --git a/examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/migration.ts b/examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/migration.ts index eefe7582fe..2dc5263d2c 100755 --- a/examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/migration.ts +++ b/examples/retail-store/migrations/app/20260628T0931_add_product_status_order_type_enums/migration.ts @@ -2,14 +2,14 @@ import { MigrationCLI } from '@prisma-next/cli/migration-cli'; import { Migration } from '@prisma-next/family-mongo/migration'; import { collMod } from '@prisma-next/target-mongo/migration'; +import type { Contract as End } from './end-contract'; +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as Start } from './start-contract'; +import startContract from './start-contract.json' with { type: 'json' }; -class M extends Migration { - override describe() { - return { - from: 'sha256:71f1cc5c3f4de1ea7c9c8426fde682cd78c7c005f6688f58c2d9d6ddd8b2284c', - to: 'sha256:24e1562cabc8241f7fd50b830ce29ea955b5cd668488fbfb5d6744b48d174d14', - }; - } +class M extends Migration { + override readonly startContractJson = startContract; + override readonly endContractJson = endContract; override get operations() { return [ From 9288a10c593b10ceb9f23fc6d0bc99b8fda8a587 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 1 Jul 2026 16:34:15 +0200 Subject: [PATCH 11/12] =?UTF-8?q?fix(tml-2892):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20DRY=20view=20getters,=20structured=20error,=20drop?= =?UTF-8?q?=20codemod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: willbot Signed-off-by: Will Madden --- .../3-tooling/migration/src/errors.ts | 16 +++ .../migration/src/exports/migration.ts | 2 +- .../3-tooling/migration/src/migration-base.ts | 67 +++++++-- .../migration/test/migration-base.test.ts | 10 +- packages/2-mongo-family/9-family/package.json | 2 +- .../9-family/src/core/mongo-migration.ts | 48 +++---- ...stance.descriptor-self-consistency.test.ts | 34 ++--- .../9-family/test/control.test.ts | 2 +- .../test/mongo-contract-json-fixture.ts | 25 +++- .../mongo-contract-serializer-base.test.ts | 4 +- .../9-family/test/mongo-migration.test.ts | 7 +- .../test/mongo-schema-verifier-base.test.ts | 2 +- .../9-family/test/schema-verify.test.ts | 2 - .../9-family/test/test-target-descriptor.ts | 10 ++ .../2-mongo-family/9-family/tsconfig.json | 2 +- .../9-family/tsconfig.test.json | 16 --- .../src/core/render-typescript.ts | 33 ++--- .../src/core/migrations/postgres-migration.ts | 39 +++-- .../postgres/test/postgres-migration.test.ts | 7 +- .../src/core/migrations/sqlite-migration.ts | 41 +++--- .../sqlite/test/sqlite-migration.test.ts | 7 +- scripts/codemod-migration-shape.mjs | 135 ------------------ 22 files changed, 210 insertions(+), 301 deletions(-) delete mode 100644 packages/2-mongo-family/9-family/tsconfig.test.json delete mode 100644 scripts/codemod-migration-shape.mjs diff --git a/packages/1-framework/3-tooling/migration/src/errors.ts b/packages/1-framework/3-tooling/migration/src/errors.ts index e1412345c2..de05e5fa16 100644 --- a/packages/1-framework/3-tooling/migration/src/errors.ts +++ b/packages/1-framework/3-tooling/migration/src/errors.ts @@ -455,3 +455,19 @@ export function errorHashNotInGraph(hash: string, graph: MigrationGraph): Migrat }, ); } + +export function errorMigrationContractViewMissing( + className: string, + accessor: 'endContract' | 'startContract', + jsonField: 'endContractJson' | 'startContractJson', +): MigrationToolsError { + return new MigrationToolsError( + 'MIGRATION.CONTRACT_VIEW_MISSING', + `${className}.${accessor} requires ${jsonField}`, + { + why: `${className}.${accessor} was read, but this instance has no ${jsonField} to build the view from.`, + fix: `Set ${jsonField} to the migration's committed contract JSON, or avoid reading ${accessor} on a migration that overrides describe() and carries no contract.`, + details: { className, accessor, jsonField }, + }, + ); +} diff --git a/packages/1-framework/3-tooling/migration/src/exports/migration.ts b/packages/1-framework/3-tooling/migration/src/exports/migration.ts index 4f2e3ecb8f..98ae9e510b 100644 --- a/packages/1-framework/3-tooling/migration/src/exports/migration.ts +++ b/packages/1-framework/3-tooling/migration/src/exports/migration.ts @@ -1,8 +1,8 @@ export { buildMigrationArtifacts, - type ContractHashInput, isDirectEntrypoint, Migration, type MigrationArtifacts, + MigrationContractViews, type MigrationMeta, } from '../migration-base'; diff --git a/packages/1-framework/3-tooling/migration/src/migration-base.ts b/packages/1-framework/3-tooling/migration/src/migration-base.ts index c8291839b2..39d786e9b9 100644 --- a/packages/1-framework/3-tooling/migration/src/migration-base.ts +++ b/packages/1-framework/3-tooling/migration/src/migration-base.ts @@ -7,7 +7,7 @@ import type { MigrationPlanOperation, } from '@prisma-next/framework-components/control'; import { type } from 'arktype'; -import { errorInvalidOperationEntry } from './errors'; +import { errorInvalidOperationEntry, errorMigrationContractViewMissing } from './errors'; import { computeMigrationHash } from './hash'; import { deriveProvidedInvariants } from './invariants'; import type { MigrationMetadata } from './metadata'; @@ -19,18 +19,6 @@ export interface MigrationMeta { readonly to: string; } -/** - * The minimal structural shape `describe()` reads off a migration's contract - * JSON input: just `storage.storageHash`. A raw `contract.json` import - * structurally satisfies this, so subclasses assign their JSON imports to - * `startContractJson`/`endContractJson` WITHOUT a cast. The full `Start`/`End` - * contract typing is applied downstream in the family bases' view getters (via - * `ContractView.fromJson<…>`), not on these raw fields. - */ -export interface ContractHashInput { - readonly storage: { readonly storageHash: string }; -} - // `from` rejects empty strings to mirror `MigrationMetadataSchema` in // `./io.ts`. Without this match, an authored migration could `describe()` with // `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have @@ -76,8 +64,14 @@ export abstract class Migration< * import). When set, the derived `describe()` reads `to` from its * `storage.storageHash`. Family bases build the typed `endContract` view from * it. Optional so `describe()`-overriding migrations (no contract) compile. + * + * Typed with a plain `storageHash: string`, not the branded + * `StorageHashBase`, so a raw `contract.json` import — whose `storageHash` + * is an untyped string literal — is assignable without a cast. The full + * `Start`/`End` contract typing is applied downstream in the family bases' + * view getters (via `ContractView.fromJson<…>`). */ - readonly endContractJson?: ContractHashInput; + readonly endContractJson?: { readonly storage: { readonly storageHash: string } }; /** * The migration's start-state contract JSON (the committed @@ -85,7 +79,7 @@ export abstract class Migration< * derives to `null`). Family bases build the typed `startContract` view from * it. */ - readonly startContractJson?: ContractHashInput; + readonly startContractJson?: { readonly storage: { readonly storageHash: string } }; /** * Assembled `ControlStack` injected by the orchestrator (`runMigration`). @@ -143,6 +137,49 @@ export abstract class Migration< } } +/** + * Lazy-memoized `endContract` / `startContract` view accessors, one instance + * held per migration. Each target base (`MongoMigration`, `SqliteMigration`, + * `PostgresMigration`) creates one `MigrationContractViews` field from + * `this` — passing its own `ContractView.fromJson<…>` as `fromJson` + * and a name for error messages — and forwards its + * `endContract`/`startContract` getters to it. + * + * `endContract` throws `MIGRATION.CONTRACT_VIEW_MISSING` when the migration + * has no `endContractJson` (mirrors `describe()`'s own requirement). + * `startContract` returns `null` for a baseline migration (no + * `startContractJson`) instead of throwing. + */ +export class MigrationContractViews { + #endView?: TView; + #startView?: TView | null; + + constructor( + private readonly migration: Migration, + private readonly className: string, + private readonly fromJson: (json: unknown) => TView, + ) {} + + get endContract(): TView { + if (this.#endView === undefined) { + const json = this.migration.endContractJson; + if (json === undefined) { + throw errorMigrationContractViewMissing(this.className, 'endContract', 'endContractJson'); + } + this.#endView = this.fromJson(json); + } + return this.#endView; + } + + get startContract(): TView | null { + if (this.#startView === undefined) { + const json = this.migration.startContractJson; + this.#startView = json === undefined ? null : this.fromJson(json); + } + return this.#startView; + } +} + /** * Returns true when `import.meta.url` resolves to the same file that was * invoked as the node entrypoint (`process.argv[1]`). Used by diff --git a/packages/1-framework/3-tooling/migration/test/migration-base.test.ts b/packages/1-framework/3-tooling/migration/test/migration-base.test.ts index 1727ed9167..965dcf15af 100644 --- a/packages/1-framework/3-tooling/migration/test/migration-base.test.ts +++ b/packages/1-framework/3-tooling/migration/test/migration-base.test.ts @@ -3,8 +3,8 @@ import { describe, expect, it } from 'vitest'; import type { MigrationMetadata } from '../src/metadata'; import { buildMigrationArtifacts, Migration } from '../src/migration-base'; -describe('Migration', async () => { - describe('operations + describe() contract', async () => { +describe('Migration', () => { + describe('operations + describe() contract', () => { it('subclasses expose operations via the getter and describe() metadata', async () => { class TestMigration extends Migration<{ id: string; @@ -76,7 +76,7 @@ describe('Migration', async () => { }); }); - describe('derived describe() from contract JSON', async () => { + describe('derived describe() from contract JSON', () => { const endJson = { storage: { storageHash: 'sha256:endhash' } }; const startJson = { storage: { storageHash: 'sha256:starthash' } }; @@ -133,7 +133,7 @@ describe('Migration', async () => { }); }); - describe('constructor accepts and stores a ControlStack', async () => { + describe('constructor accepts and stores a ControlStack', () => { /** * The constructor injection contract is that a subclass (e.g. * `PostgresMigration`) can read `this.stack` to materialize whatever it @@ -187,7 +187,7 @@ describe('Migration', async () => { * dry-run stdout output) lives in `@prisma-next/cli` and is exercised * there. */ -describe('buildMigrationArtifacts', async () => { +describe('buildMigrationArtifacts', () => { function makeMigration( operations: unknown, meta: { diff --git a/packages/2-mongo-family/9-family/package.json b/packages/2-mongo-family/9-family/package.json index abc58f3068..22830aa02c 100644 --- a/packages/2-mongo-family/9-family/package.json +++ b/packages/2-mongo-family/9-family/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "tsdown", "test": "vitest run --passWithNoTests", - "typecheck": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json --noEmit", + "typecheck": "tsc --project tsconfig.json --noEmit", "lint": "biome check . --error-on-warnings", "clean": "rm -rf dist coverage" }, diff --git a/packages/2-mongo-family/9-family/src/core/mongo-migration.ts b/packages/2-mongo-family/9-family/src/core/mongo-migration.ts index 0a4131a241..34af6a5064 100644 --- a/packages/2-mongo-family/9-family/src/core/mongo-migration.ts +++ b/packages/2-mongo-family/9-family/src/core/mongo-migration.ts @@ -1,4 +1,4 @@ -import { Migration } from '@prisma-next/migration-tools/migration'; +import { Migration, MigrationContractViews } from '@prisma-next/migration-tools/migration'; import type { MongoContract } from '@prisma-next/mongo-contract'; import type { AnyMongoMigrationOperation } from '@prisma-next/mongo-query-ast/control'; import { MongoContractView } from './ir/mongo-contract-view'; @@ -21,9 +21,10 @@ import { MongoContractView } from './ir/mongo-contract-view'; * that assigns its `start-contract.json` / `end-contract.json` imports gets * fully-typed view accessors: `this.endContract` is a `MongoContractView` * (and `this.startContract` a `MongoContractView | null`), built lazily - * from those JSON fields. The framework base derives `describe()` from the same - * JSON. View getters live on the family base (not the framework base) because - * `MongoContractView`'s shape is Mongo-specific. + * from those JSON fields via the shared `MigrationContractViews` helper. The + * framework base derives `describe()` from the same JSON. View getters live on + * the family base (not the framework base) because `MongoContractView`'s shape + * is Mongo-specific. */ export abstract class MongoMigration< Start extends MongoContract = MongoContract, @@ -31,39 +32,30 @@ export abstract class MongoMigration< > extends Migration { readonly targetId = 'mongo' as const; - #endContract?: MongoContractView; - #startContract?: MongoContractView | null; + #endView = new MigrationContractViews>(this, 'MongoMigration', (json) => + MongoContractView.fromJson(json), + ); + #startView = new MigrationContractViews>( + this, + 'MongoMigration', + (json) => MongoContractView.fromJson(json), + ); /** * The typed, namespace-unwrapped view over this migration's end-state - * contract — `this.endContract.collection..validator`, etc. Lazily - * built from `endContractJson` and memoized. Throws if no `endContractJson` - * was provided (a `describe()`-overriding migration with no contract has no - * view to expose). + * contract — `this.endContract.collection..validator`, etc. Throws if + * no `endContractJson` was provided (a `describe()`-overriding migration + * with no contract has no view to expose). */ get endContract(): MongoContractView { - if (this.#endContract === undefined) { - if (this.endContractJson === undefined) { - throw new Error( - 'MongoMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', - ); - } - this.#endContract = MongoContractView.fromJson(this.endContractJson); - } - return this.#endContract; + return this.#endView.endContract; } /** - * The typed view over this migration's start-state contract, or `null` for a - * baseline migration (no `startContractJson`). Lazily built and memoized. + * The typed view over this migration's start-state contract, or `null` for + * a baseline migration (no `startContractJson`). */ get startContract(): MongoContractView | null { - if (this.#startContract === undefined) { - this.#startContract = - this.startContractJson === undefined - ? null - : MongoContractView.fromJson(this.startContractJson); - } - return this.#startContract; + return this.#startView.startContract; } } diff --git a/packages/2-mongo-family/9-family/test/control-instance.descriptor-self-consistency.test.ts b/packages/2-mongo-family/9-family/test/control-instance.descriptor-self-consistency.test.ts index b47f6b8817..dcbbcd5c9b 100644 --- a/packages/2-mongo-family/9-family/test/control-instance.descriptor-self-consistency.test.ts +++ b/packages/2-mongo-family/9-family/test/control-instance.descriptor-self-consistency.test.ts @@ -4,8 +4,9 @@ import type { ContractSpace, ControlStack } from '@prisma-next/framework-compone import { createControlStack } from '@prisma-next/framework-components/control'; import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; import { MigrationToolsError } from '@prisma-next/migration-tools/errors'; -import type { MongoContract, MongoStorage } from '@prisma-next/mongo-contract'; +import { buildMongoNamespace, type MongoContract, MongoStorage } from '@prisma-next/mongo-contract'; import { mongoContractCanonicalizationHooks } from '@prisma-next/mongo-contract/canonicalization-hooks'; +import { applicationDomainOf } from '@prisma-next/test-utils'; import { describe, expect, it } from 'vitest'; import { mongoFamilyDescriptor } from '../src/core/control-descriptor'; import { createMongoFamilyInstance } from '../src/core/control-instance'; @@ -15,25 +16,24 @@ import { stubMongoTargetDescriptor as mongoTargetDescriptor } from './test-targe const TARGET = 'mongo' as const; const TARGET_FAMILY = 'mongo' as const; -const fixtureStorageBody = { - namespaces: { - [UNBOUND_NAMESPACE_ID]: { - id: UNBOUND_NAMESPACE_ID, - entries: { - collection: { - fixture_box: { - indexes: [{ keys: [{ field: 'email', direction: 1 as const }], unique: true }], - }, - }, +const fixtureNamespace = buildMongoNamespace({ + id: UNBOUND_NAMESPACE_ID, + entries: { + collection: { + fixture_box: { + indexes: [{ keys: [{ field: 'email', direction: 1 }], unique: true }], }, }, }, -}; +}); +// Hash over the constructed namespace (not a hand-written literal): the +// self-consistency check recomputes the hash from this same `MongoStorage` +// instance's fields, so both sides must derive from identical data. const FIXTURE_HEAD_HASH = computeStorageHash({ target: TARGET, targetFamily: TARGET_FAMILY, - storage: fixtureStorageBody, + storage: { namespaces: { [UNBOUND_NAMESPACE_ID]: fixtureNamespace } }, ...mongoContractCanonicalizationHooks, }); @@ -42,15 +42,15 @@ function buildContract(): MongoContract { target: TARGET, targetFamily: TARGET_FAMILY, roots: {}, - models: {}, + domain: applicationDomainOf({}), capabilities: {}, extensionPacks: {}, meta: {}, profileHash: profileHash('fixture-profile-v1'), - storage: { - ...fixtureStorageBody, + storage: new MongoStorage({ storageHash: coreHash(FIXTURE_HEAD_HASH), - }, + namespaces: { [UNBOUND_NAMESPACE_ID]: fixtureNamespace }, + }), }; } diff --git a/packages/2-mongo-family/9-family/test/control.test.ts b/packages/2-mongo-family/9-family/test/control.test.ts index e81cca0dc9..503b1bc30e 100644 --- a/packages/2-mongo-family/9-family/test/control.test.ts +++ b/packages/2-mongo-family/9-family/test/control.test.ts @@ -76,7 +76,7 @@ describe('createMongoFamilyInstance', () => { expect(() => instance.verifySchema({ contract: {}, - schema: { collections: [] } as MongoSchemaIR, + schema: new MongoSchemaIR([]), strict: false, frameworkComponents: [], }), diff --git a/packages/2-mongo-family/9-family/test/mongo-contract-json-fixture.ts b/packages/2-mongo-family/9-family/test/mongo-contract-json-fixture.ts index 86124ed26d..ba6d0a872e 100644 --- a/packages/2-mongo-family/9-family/test/mongo-contract-json-fixture.ts +++ b/packages/2-mongo-family/9-family/test/mongo-contract-json-fixture.ts @@ -6,17 +6,30 @@ import { import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; import { applicationDomainOf } from '@prisma-next/test-utils'; +function normalizeModels( + models: Record, +): Record { + return Object.fromEntries( + Object.entries(models).map(([name, model]) => [ + name, + { ...model, relations: model.relations ?? {} }, + ]), + ) as Record; +} + export function mongoContractJson(params: { readonly models?: Record; readonly storageCollections?: Record; readonly roots?: Record>; }) { - const models = params.models ?? { - Item: { - fields: { _id: { type: { kind: 'scalar', codecId: 'mongo/objectId@1' }, nullable: false } }, - storage: { collection: 'items' }, - }, - }; + const models = normalizeModels( + (params.models ?? { + Item: { + fields: { _id: { type: { kind: 'scalar', codecId: 'mongo/objectId@1' }, nullable: false } }, + storage: { collection: 'items' }, + }, + }) as Record, + ); const collections = params.storageCollections ?? { items: {} }; return { targetFamily: 'mongo', diff --git a/packages/2-mongo-family/9-family/test/mongo-contract-serializer-base.test.ts b/packages/2-mongo-family/9-family/test/mongo-contract-serializer-base.test.ts index d52d7353bc..afc5705bfe 100644 --- a/packages/2-mongo-family/9-family/test/mongo-contract-serializer-base.test.ts +++ b/packages/2-mongo-family/9-family/test/mongo-contract-serializer-base.test.ts @@ -30,7 +30,7 @@ describe('MongoContractSerializerBase', () => { const result = serializer.deserializeContract(json); expect(result.contract.targetFamily).toBe('mongo'); - expect(result.contract.domain.namespaces.__unbound__!.models['Item']).toBeDefined(); + expect(result.contract.domain.namespaces['__unbound__']!.models['Item']).toBeDefined(); }); it('rejects non-Mongo targetFamily', () => { @@ -49,6 +49,7 @@ describe('MongoContractSerializerBase', () => { _id: { type: { kind: 'scalar', codecId: 'mongo/objectId@1' }, nullable: false }, }, storage: { collection: 'missing_collection' }, + relations: {}, }, }, }); @@ -66,6 +67,7 @@ describe('MongoContractSerializerBase', () => { data: { type: { kind: 'valueObject', name: 'Missing' }, nullable: false }, }, storage: { collection: 'items' }, + relations: {}, }, }, }); diff --git a/packages/2-mongo-family/9-family/test/mongo-migration.test.ts b/packages/2-mongo-family/9-family/test/mongo-migration.test.ts index ad219f0b3c..5768629981 100644 --- a/packages/2-mongo-family/9-family/test/mongo-migration.test.ts +++ b/packages/2-mongo-family/9-family/test/mongo-migration.test.ts @@ -1,3 +1,4 @@ +import type { MigrationToolsError } from '@prisma-next/migration-tools/errors'; import { describe, expect, it } from 'vitest'; import { MongoMigration } from '../src/core/mongo-migration'; import type { Contract } from './fixtures/migration-end-contract.d'; @@ -68,6 +69,10 @@ describe('MongoMigration view getters', () => { return []; } } - expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + expect(() => new NoContract().endContract).toThrowError( + expect.objectContaining({ + code: 'MIGRATION.CONTRACT_VIEW_MISSING', + }) as unknown as MigrationToolsError, + ); }); }); diff --git a/packages/2-mongo-family/9-family/test/mongo-schema-verifier-base.test.ts b/packages/2-mongo-family/9-family/test/mongo-schema-verifier-base.test.ts index 7e506cfdc3..00405b6803 100644 --- a/packages/2-mongo-family/9-family/test/mongo-schema-verifier-base.test.ts +++ b/packages/2-mongo-family/9-family/test/mongo-schema-verifier-base.test.ts @@ -7,13 +7,13 @@ import { MongoSchemaVerifierBase } from '../src/core/ir/mongo-schema-verifier-ba class FakeNamespace implements Namespace { readonly kind = 'fake-namespace' as const; + readonly entries = {}; constructor(readonly id: string) {} } function makeFakeStorage(namespaces: Readonly>): MongoStorage { return new MongoStorage({ storageHash: coreHash('fake-hash'), - collections: {}, namespaces, }); } diff --git a/packages/2-mongo-family/9-family/test/schema-verify.test.ts b/packages/2-mongo-family/9-family/test/schema-verify.test.ts index 471d74eb76..8afddf97f0 100644 --- a/packages/2-mongo-family/9-family/test/schema-verify.test.ts +++ b/packages/2-mongo-family/9-family/test/schema-verify.test.ts @@ -642,7 +642,6 @@ describe('verifyMongoSchema', () => { maxVariable: 'punct', normalization: false, numericOrdering: false, - version: '57.1', }, }), ], @@ -717,7 +716,6 @@ describe('verifyMongoSchema', () => { maxVariable: 'punct', normalization: false, numericOrdering: false, - version: '57.1', }, }), }), diff --git a/packages/2-mongo-family/9-family/test/test-target-descriptor.ts b/packages/2-mongo-family/9-family/test/test-target-descriptor.ts index a4aed01dce..ddcd85e316 100644 --- a/packages/2-mongo-family/9-family/test/test-target-descriptor.ts +++ b/packages/2-mongo-family/9-family/test/test-target-descriptor.ts @@ -14,6 +14,16 @@ export const stubMongoTargetDescriptor: ControlTargetDescriptor<'mongo', 'mongo' familyId: 'mongo', targetId: 'mongo', version: '0.0.1', + // Tests using this stub reject before any real serialization runs (the + // family instance deserializes contracts itself, not via this slot). + contractSerializer: { + deserializeContract() { + throw new Error('stubMongoTargetDescriptor has no contract serializer'); + }, + serializeContract() { + throw new Error('stubMongoTargetDescriptor has no contract serializer'); + }, + }, create() { return { familyId: 'mongo', targetId: 'mongo' }; }, diff --git a/packages/2-mongo-family/9-family/tsconfig.json b/packages/2-mongo-family/9-family/tsconfig.json index d01ca3929b..c8f137dec8 100644 --- a/packages/2-mongo-family/9-family/tsconfig.json +++ b/packages/2-mongo-family/9-family/tsconfig.json @@ -5,6 +5,6 @@ "outDir": "./dist", "skipLibCheck": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "test/**/*.ts"], "exclude": ["dist"] } diff --git a/packages/2-mongo-family/9-family/tsconfig.test.json b/packages/2-mongo-family/9-family/tsconfig.test.json deleted file mode 100644 index 73d1e0b4f3..0000000000 --- a/packages/2-mongo-family/9-family/tsconfig.test.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["@prisma-next/tsconfig/base"], - "compilerOptions": { - "rootDir": ".", - "outDir": "dist", - "skipLibCheck": true, - "noEmit": true - }, - "include": [ - "src/**/*.ts", - "test/mongo-contract-view.test.ts", - "test/mongo-contract-view.test-d.ts", - "test/mongo-migration.test.ts" - ], - "exclude": ["dist"] -} diff --git a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts index fa8cc31001..be55432272 100644 --- a/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts +++ b/packages/3-mongo-target/1-mongo-target/src/core/render-typescript.ts @@ -29,29 +29,20 @@ const BASE_IMPORTS: readonly ImportRequirement[] = [ ]; /** - * Render a list of Mongo `OpFactoryCall`s as a `migration.ts` - * source string. The result is shebanged, extends the user-facing - * `Migration` (i.e. `MongoMigration`) from `@prisma-next/family-mongo`, and - * implements `operations`. - * - * The scaffold does NOT render a `describe()` method. Instead it imports the - * committed contract JSON (`end-contract.json`, plus `start-contract.json` for a - * non-baseline migration) and assigns it to the `endContractJson` / - * `startContractJson` fields; the `Migration` base derives `describe()`'s - * from/to from those JSONs' `storage.storageHash`. The class is parameterised - * `Migration` (or `Migration` for a baseline, where - * `meta.from === null` — no start contract). `meta.to` is no longer embedded as - * a literal (the base reads it from the JSON at runtime); `meta.from` only - * selects the baseline vs non-baseline shape. + * Render a list of Mongo `OpFactoryCall`s as a `migration.ts` source string. + * The result is shebanged, imports the committed contract JSON + * (`end-contract.json`, plus `start-contract.json` for a non-baseline + * migration), extends `Migration` (or `Migration` for + * a baseline) from `@prisma-next/family-mongo`, assigns the JSON to + * `endContractJson` / `startContractJson`, and implements `operations`. The + * `Migration` base derives `describe()` from those fields. * * The walk is polymorphic: each call node contributes its own - * `renderTypeScript()` expression and declares its own - * `importRequirements()`. The top-level renderer aggregates imports - * across all nodes and emits one `import { … } from "…"` line per module. - * The `Migration` / `MigrationCLI` base imports and the contract-JSON imports - * are always emitted — they're structural to the rendered scaffold (extends - * `Migration`, derives `describe()` from the JSON, calls `MigrationCLI.run`), - * not driven by any node. + * `renderTypeScript()` expression and declares its own `importRequirements()`. + * The top-level renderer aggregates imports across all nodes and emits one + * `import { … } from "…"` line per module. The `Migration` / `MigrationCLI` + * base imports and the contract-JSON imports are always emitted, independent + * of the call nodes. */ export function renderCallsToTypeScript( calls: ReadonlyArray, diff --git a/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts b/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts index 527f426b86..20f1a173f6 100644 --- a/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts +++ b/packages/3-targets/3-targets/postgres/src/core/migrations/postgres-migration.ts @@ -3,6 +3,7 @@ import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control' import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter'; import { Migration as SqlMigration } from '@prisma-next/family-sql/migration'; import type { ControlStack } from '@prisma-next/framework-components/control'; +import { MigrationContractViews } from '@prisma-next/migration-tools/migration'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast'; import { errorPostgresMigrationStackMissing } from '../errors'; @@ -75,8 +76,16 @@ export abstract class PostgresMigration< */ protected readonly controlAdapter: SqlControlAdapter<'postgres'> | undefined; - #endContract?: PostgresContractView; - #startContract?: PostgresContractView | null; + #endView = new MigrationContractViews>( + this, + 'PostgresMigration', + (json) => PostgresContractView.fromJson(json), + ); + #startView = new MigrationContractViews>( + this, + 'PostgresMigration', + (json) => PostgresContractView.fromJson(json), + ); constructor(stack?: ControlStack<'sql', 'postgres'>) { super(stack); @@ -90,35 +99,19 @@ export abstract class PostgresMigration< /** * The typed, schema-qualified Postgres view over this migration's end-state - * contract — `this.endContract.namespace..table.`, etc. Lazily - * built from `endContractJson` and memoized. Throws if no `endContractJson` - * was provided. + * contract — `this.endContract.namespace..table.`, etc. Throws + * if no `endContractJson` was provided. */ get endContract(): PostgresContractView { - if (this.#endContract === undefined) { - if (this.endContractJson === undefined) { - throw new Error( - 'PostgresMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', - ); - } - this.#endContract = PostgresContractView.fromJson(this.endContractJson); - } - return this.#endContract; + return this.#endView.endContract; } /** * The typed Postgres view over this migration's start-state contract, or - * `null` for a baseline migration (no `startContractJson`). Lazily built and - * memoized. + * `null` for a baseline migration (no `startContractJson`). */ get startContract(): PostgresContractView | null { - if (this.#startContract === undefined) { - this.#startContract = - this.startContractJson === undefined - ? null - : PostgresContractView.fromJson(this.startContractJson); - } - return this.#startContract; + return this.#startView.startContract; } /** diff --git a/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts index 1616c12917..ad7d7e25b5 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-migration.test.ts @@ -1,3 +1,4 @@ +import type { MigrationToolsError } from '@prisma-next/migration-tools/errors'; import { describe, expect, it } from 'vitest'; import { PostgresMigration } from '../src/core/migrations/postgres-migration'; import type { Contract } from './fixtures/namespaced-contract.d'; @@ -69,6 +70,10 @@ describe('PostgresMigration view getters', () => { return []; } } - expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + expect(() => new NoContract().endContract).toThrowError( + expect.objectContaining({ + code: 'MIGRATION.CONTRACT_VIEW_MISSING', + }) as unknown as MigrationToolsError, + ); }); }); diff --git a/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts b/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts index 04cd8c7f27..0fab35c76c 100644 --- a/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts +++ b/packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts @@ -6,6 +6,7 @@ import type { import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter'; import { Migration as SqlMigration } from '@prisma-next/family-sql/migration'; import type { ControlStack } from '@prisma-next/framework-components/control'; +import { MigrationContractViews } from '@prisma-next/migration-tools/migration'; import type { SqlStorage } from '@prisma-next/sql-contract/types'; import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast'; import { blindCast } from '@prisma-next/utils/casts'; @@ -42,8 +43,9 @@ type Op = SqlMigrationPlanOperation; * that assigns its `start-contract.json` / `end-contract.json` imports gets * fully-typed view accessors: `this.endContract` is a `SqliteContractView` * (sole namespace unwrapped to the root — `this.endContract.table.`), - * built lazily from the JSON fields and memoized. Mirrors `MongoMigration`'s - * view getters; the framework base derives `describe()` from the same JSON. + * built lazily from the JSON fields via the shared `MigrationContractViews` + * helper. Mirrors `MongoMigration`'s view getters; the framework base derives + * `describe()` from the same JSON. */ export abstract class SqliteMigration< Start extends Contract = Contract, @@ -59,8 +61,14 @@ export abstract class SqliteMigration< */ protected readonly controlAdapter: SqlControlAdapter<'sqlite'> | undefined; - #endContract?: SqliteContractView; - #startContract?: SqliteContractView | null; + #endView = new MigrationContractViews>(this, 'SqliteMigration', (json) => + SqliteContractView.fromJson(json), + ); + #startView = new MigrationContractViews>( + this, + 'SqliteMigration', + (json) => SqliteContractView.fromJson(json), + ); constructor(stack?: ControlStack<'sql', 'sqlite'>) { super(stack); @@ -75,33 +83,18 @@ export abstract class SqliteMigration< /** * The typed SQLite view over this migration's end-state contract — sole * namespace unwrapped to the root, so `this.endContract.table.` etc. - * Lazily built from `endContractJson` and memoized. Throws if no - * `endContractJson` was provided. + * Throws if no `endContractJson` was provided. */ get endContract(): SqliteContractView { - if (this.#endContract === undefined) { - if (this.endContractJson === undefined) { - throw new Error( - 'SqliteMigration.endContract: no endContractJson provided — set endContractJson to read the end-state contract view.', - ); - } - this.#endContract = SqliteContractView.fromJson(this.endContractJson); - } - return this.#endContract; + return this.#endView.endContract; } /** - * The typed SQLite view over this migration's start-state contract, or `null` - * for a baseline migration (no `startContractJson`). Lazily built and memoized. + * The typed SQLite view over this migration's start-state contract, or + * `null` for a baseline migration (no `startContractJson`). */ get startContract(): SqliteContractView | null { - if (this.#startContract === undefined) { - this.#startContract = - this.startContractJson === undefined - ? null - : SqliteContractView.fromJson(this.startContractJson); - } - return this.#startContract; + return this.#startView.startContract; } protected createTable(options: { diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts index b3966e669d..661d72ef06 100644 --- a/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-migration.test.ts @@ -1,3 +1,4 @@ +import type { MigrationToolsError } from '@prisma-next/migration-tools/errors'; import { describe, expect, it } from 'vitest'; import { SqliteMigration } from '../src/core/migrations/sqlite-migration'; import type { Contract } from './fixtures/sqlite-contract.d'; @@ -70,6 +71,10 @@ describe('SqliteMigration view getters', () => { return []; } } - expect(() => new NoContract().endContract).toThrow(/no endContractJson/); + expect(() => new NoContract().endContract).toThrowError( + expect.objectContaining({ + code: 'MIGRATION.CONTRACT_VIEW_MISSING', + }) as unknown as MigrationToolsError, + ); }); }); diff --git a/scripts/codemod-migration-shape.mjs b/scripts/codemod-migration-shape.mjs deleted file mode 100644 index 0dad69c110..0000000000 --- a/scripts/codemod-migration-shape.mjs +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot codemod: convert a committed example `migration.ts` from the old - * hand-written `describe()` scaffold to the new contract-JSON scaffold that the - * migration generator (`renderCallsToTypeScript`, TML-2892) now emits. - * - * The transform is source-to-source and behavior-preserving: - * - The `operations` getter body is kept verbatim (so `ops.json` is - * byte-identical after the migration re-emits). - * - `describe()` is removed; its `from`/`to` only decide the baseline shape. - * The base derives describe() from the imported contract JSON, whose - * `storage.storageHash` already equals the committed from/to hashes. - * - Contract-JSON imports + `endContractJson` / `startContractJson` fields are - * injected; the class header gets `` (or `` baseline). - * - * Idempotent: a migration already on the new shape (`endContractJson` field, no - * `describe()`) is left untouched. - * - * Final formatting is NOT done here — the caller runs `biome check --write` so - * the output matches the generator's biome-formatted shape exactly. - * - * Usage: node scripts/codemod-migration-shape.mjs [ ...] - */ - -import { readFileSync, writeFileSync } from 'node:fs'; - -/** Extract the `from:`/`to:` from a `describe()` return literal. */ -function parseDescribe(src) { - // Match `override describe() { return { from: , to: ... }; }` - const fromMatch = src.match(/\bfrom:\s*(null|['"]sha256:[0-9a-f]+['"])/); - const toMatch = src.match(/\bto:\s*['"](sha256:[0-9a-f]+)['"]/); - if (!toMatch) { - throw new Error('codemod: no `to: "sha256:..."` found in describe()'); - } - const isBaseline = !fromMatch || fromMatch[1] === 'null'; - return { isBaseline }; -} - -/** Remove the whole `override describe() { ... }` method (brace-balanced). */ -function stripDescribe(src) { - const start = src.search(/\n[ \t]*override describe\(\)\s*\{/); - if (start === -1) return src; - // find the method's opening brace, then balance. - let i = src.indexOf('{', start); - let depth = 0; - for (; i < src.length; i++) { - if (src[i] === '{') depth++; - else if (src[i] === '}') { - depth--; - if (depth === 0) break; - } - } - // i is at the closing brace of the method. Drop from `start` (the leading - // newline) through the closing brace and any trailing blank line. - let end = i + 1; - if (src.slice(end).startsWith('\n\n')) end += 1; // collapse one blank line - return src.slice(0, start) + src.slice(end); -} - -function transform(path) { - let src = readFileSync(path, 'utf8'); - - if (src.includes('endContractJson =') && !/\boverride describe\(\)/.test(src)) { - return { path, changed: false, reason: 'already new shape' }; - } - if (!/\boverride describe\(\)/.test(src)) { - return { path, changed: false, reason: 'no describe() to convert (skipped)' }; - } - - const { isBaseline } = parseDescribe(src); - - // 1. Strip describe(). - src = stripDescribe(src); - - // 2. Inject contract-JSON imports after the shebang (biome re-sorts later). - const contractImports = [ - `import type { Contract as End } from './end-contract';`, - `import endContract from './end-contract.json' with { type: 'json' };`, - ...(isBaseline - ? [] - : [ - `import type { Contract as Start } from './start-contract';`, - `import startContract from './start-contract.json' with { type: 'json' };`, - ]), - ].join('\n'); - - const shebang = '#!/usr/bin/env -S node\n'; - if (src.startsWith(shebang)) { - src = shebang + contractImports + '\n' + src.slice(shebang.length); - } else { - src = contractImports + '\n' + src; - } - - // 3. Add the `` / `` generic to the class header and - // inject the field(s) right after the opening brace. - const generics = isBaseline ? '' : ''; - const fields = isBaseline - ? ' override readonly endContractJson = endContract;\n\n' - : ' override readonly startContractJson = startContract;\n override readonly endContractJson = endContract;\n\n'; - - const classRe = /((?:export default )?class \w+ extends Migration)(\s*\{)/; - if (!classRe.test(src)) { - throw new Error('codemod: no `class … extends Migration {` header found'); - } - src = src.replace( - classRe, - (_m, head, brace) => `${head}${generics}${brace.replace(/\s*\{/, ' {')}\n${fields}`, - ); - - writeFileSync(path, src, 'utf8'); - return { path, changed: true, isBaseline }; -} - -function main() { - const paths = process.argv.slice(2); - if (paths.length === 0) { - console.error('usage: node scripts/codemod-migration-shape.mjs ...'); - process.exit(2); - } - let changed = 0; - for (const p of paths) { - const r = transform(p); - if (r.changed) { - changed++; - console.log( - `codemod: rewrote ${p} (${r.isBaseline ? 'baseline ' : 'with-start '})`, - ); - } else { - console.log(`codemod: skipped ${p} (${r.reason})`); - } - } - console.log(`codemod: ${changed}/${paths.length} migration(s) rewritten`); -} - -main(); From eb2c3698456eae74f330972a85b1a866e1721d88 Mon Sep 17 00:00:00 2001 From: willbot Date: Wed, 1 Jul 2026 16:40:31 +0200 Subject: [PATCH 12/12] docs(adr): ADR 232 migration-authoring model + ADR 233 ContractView 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 Signed-off-by: willbot Signed-off-by: Will Madden --- ...st its start and end contract snapshots.md | 58 +++++++++++++++ ... typed by-name accessor over a contract.md | 73 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 docs/architecture docs/adrs/ADR 232 - A migration is authored against its start and end contract snapshots.md create mode 100644 docs/architecture docs/adrs/ADR 233 - ContractView is a typed by-name accessor over a contract.md diff --git a/docs/architecture docs/adrs/ADR 232 - A migration is authored against its start and end contract snapshots.md b/docs/architecture docs/adrs/ADR 232 - A migration is authored against its start and end contract snapshots.md new file mode 100644 index 0000000000..c913d5a0da --- /dev/null +++ b/docs/architecture docs/adrs/ADR 232 - A migration is authored against its start and end contract snapshots.md @@ -0,0 +1,58 @@ +# ADR 232 — A migration is authored against its start and end contract snapshots + +## At a glance + +A migration takes the two contract snapshots it moves between as its inputs. It assigns them to the base class and writes its operations; it states nothing else about the transition: + +```ts +import endContract from './end-contract.json' with { type: 'json' }; +import type { Contract as End } from './end-contract'; + +class M extends Migration { + override readonly endContractJson = endContract; + override get operations() { + return [ createCollection('carts', { validator: { $jsonSchema: { /* … */ } } }), /* … */ ]; + } +} +``` + +From those snapshots the base supplies the two things every migration needs: the transition's identity, and typed access to the contract by name (a `ContractView`, [ADR 233]()). + +## Decision + +A migration is the step that moves a database from one contract to the next. The state it starts from and the state it produces are what define it, so a migration takes those two contracts — its **start** and **end** snapshots — as its inputs. Each migration directory carries them as committed, immutable artifacts (`start-contract.json` / `end-contract.json` and their `.d.ts` types); the migration assigns them to `startContractJson` / `endContractJson`. + +Two things follow from holding the snapshots, and the base owns both: + +1. **Identity.** `describe()` returns `{ to: endContractJson.storage.storageHash, from: startContractJson?.storage.storageHash ?? null }`. A migration's from/to identity is a property of the two states it names, read from them directly. +2. **Typed access.** The family bases (`MongoMigration`, `SqliteMigration`, `PostgresMigration`) expose lazy, memoized `startContract` / `endContract` getters — a `ContractView` over each snapshot ([ADR 233]()) — so hand-written migration logic reaches entities by name. + +A migration that carries no contract — an extension-install migration that only issues DDL, say — overrides `describe()` directly and sets no snapshot fields. + +## Identity is read from the snapshot + +`storage.storageHash` is a property of any contract, target-independent, so identity derivation lives on the framework `Migration` base. The runner consumes a migration through `origin` / `destination`, each `{ storageHash }`, and those project straight from the snapshots the migration ships. There is a single source for a migration's identity — the snapshots in its own directory — so its declared transition and the contracts it carries cannot disagree. + +## Generated schema, hand-authored transforms + +Schema operations are a pure function of the contract diff, so the migration generator emits them in full and reads nothing from the contract at author time. The snapshots are on the class for the two things a diff cannot produce: the base's identity derivation, and hand-written logic. + +That second case — a data migration, such as a backfill — is where the typed `startContract` / `endContract` views earn their place. Its logic cannot be synthesized from a schema diff; an author writes it, and `this.endContract` is where that code reads entity metadata by name. The view is a convenience exactly where authoring happens by hand, not over coordinates that generated code never spells. + +## Consequences + +- A migration's identity is read from the contract snapshots in its own directory; there is no separate hash for an author to keep in step. +- A migration's authored scaffold is a function of its snapshots and its operations, so re-emitting the scaffold leaves the migration's behaviour — its `ops.json` / `migration.json` — unchanged. +- Every migration has typed contract access; the type flows from the per-migration `end-contract.d.ts` through the family base's view getter. +- The base carries optional snapshot fields and a concrete `describe()` that a subclass may override, so a migration with no contract is still a valid migration. + +## Alternatives considered + +- **A hand-written `describe()` carrying the from/to hashes as literals.** The identity restated as strings beside the file that already holds the contracts those strings summarize — two sources for one fact, kept in step by the author. Rejected in favour of deriving identity from the snapshots. +- **A migration that references a single shared contract rather than a per-migration snapshot pair.** A migration would then read the current contract, not the one it was authored against, and its meaning would drift as the schema evolves. Rejected: a migration is a fixed transition and must name both endpoints as immutable snapshots. + +## References + +- [ADR 233 — ContractView is a typed, by-name accessor over a contract]() (the typed access this migration model exposes). +- ADR 224 — Namespace concretions address entities by coordinate. +- ADR 223 — Target-owned default namespace. diff --git a/docs/architecture docs/adrs/ADR 233 - ContractView is a typed by-name accessor over a contract.md b/docs/architecture docs/adrs/ADR 233 - ContractView is a typed by-name accessor over a contract.md new file mode 100644 index 0000000000..48c2c92e73 --- /dev/null +++ b/docs/architecture docs/adrs/ADR 233 - ContractView is a typed by-name accessor over a contract.md @@ -0,0 +1,73 @@ +# ADR 233 — ContractView is a typed, by-name accessor over a contract + +## At a glance + +A `ContractView` reads a contract's entities by name, with the default namespace unwrapped: + +```ts +import { MongoContractView } from '@prisma-next/family-mongo/ir'; + +const view = MongoContractView.fromJson(contractJson); +view.collection.carts.validator; +``` + +instead of the entity's full storage coordinate: + +```ts +contractJson.storage.namespaces.__unbound__.entries.collection.carts.validator; +``` + +The view is a **superset of the contract** — usable anywhere the contract is — so a single value serves both roles. + +## Decision + +A contract addresses its entities by storage coordinate: `storage.namespaces..entries..`. That coordinate is exact and correct for the serialized artifact, but it is not a reading surface — it spells a namespace-binding sentinel, an `entries` dictionary, and a kind key, none of which mean anything to code that just wants "the `carts` collection." + +`ContractView` is a separate object that presents the same entities by name. It is: + +- **A superset of the contract.** The view carries the whole contract plus by-name accessors, so a caller holds one value that is both the contract and the ergonomic surface over it. +- **Per target, via a `from` / `fromJson` factory.** `from(contract)` wraps an already-deserialized contract; `fromJson(json)` deserializes and wraps in one step. The view type is generic over the contract, so access stays fully typed against the specific contract passed. +- **Default-namespace-unwrapping.** A single-namespace target reads its entities flat; a multi-namespace target keeps the namespace as a coordinate. + +The contract type itself stays a raw mirror of the serialized form. The view is a projection computed from whatever contract it is given, so the emitter and serializers own no part of it — nothing about the accessor is baked into the emitted artifact. + +## The per-target shape + +Each target's view unwraps that target's default namespace and surfaces its entity-kind slots: + +| Target | Access | Namespace handling | +| --- | --- | --- | +| Mongo | `view.collection.` | single namespace unwrapped to the root | +| SQLite | `view.table.`, `view.valueSet.` | single namespace unwrapped to the root | +| Postgres | `view.namespace..table.` | schemas addressed under a fixed `namespace` member | + +Pack-contributed entity kinds (beyond a family's built-in kinds) remain reachable under `entries` on the projected namespace, keyed by the kind's registered name. + +## Schemas are addressed under a member, not the contract root + +The view shares one namespace projection with the runtime `enums` surface: a namespace-keyed map, `view.namespace.`, with the default namespace unwrapped for single-namespace targets. A multi-namespace target keeps each schema as a coordinate under the fixed `namespace` member rather than lifting schema names onto the view's root. + +Schema names are user-chosen. A schema placed at the root and named like a contract field — `storage`, `domain` — would shadow that field, and because the view is a structural superset of the contract, the type system would not catch it. Addressing schemas under `namespace` makes the collision impossible: the only keys at the root are the contract's own fields and a family's fixed entity-kind slots, all known ahead of time. A single-namespace target still reads its entities flat, because it has exactly one namespace to unwrap and no schema coordinate to disambiguate. + +## Consequences + +- A reader of a contract reaches entities by name without spelling the namespace sentinel, the `entries` dictionary, or a kind key. +- The view is substitutable for the contract, so one value carries both the raw contract and the by-name surface. +- The projection is single-sourced with the runtime `enums` surface, so the two present namespaces the same way. +- A view over a contract that lacks the expected default namespace fails loudly at construction rather than yielding an undefined slot. + +## Alternatives considered + +- **An accessor method on the contract type itself.** The author-facing contract is data-only — its emitted `.d.ts` declares no methods — so a getter there is invisible to consumer code, and it would couple the emitter to a convenience concern. Rejected in favour of a separate view object. +- **Denormalised accessor data emitted into the contract artifact.** Duplicates every entity in the canonical artifact and invites drift between the copies. Rejected. +- **Schema names at the view's root** (a flat `view..…`). A schema named like a contract field silently shadows it, uncaught by the type system. Rejected in favour of the `namespace` member. +- **Free helper functions over the contract** (`tables(contract)`, `collections(contract)`). Workable, but scatters the surface across many functions and gives the projection no single home. Rejected in favour of one view object per target. +- **A class whose static factory returns a non-instance.** A `class` existing only to host a `static from()` that returns a plain projection shape adds a type that is never instantiated. Rejected in favour of a plain `{ from, fromJson }` factory. + +## References + +- ADR 232 — A migration is authored against its start and end contract snapshots (the view's consumer). +- ADR 224 — Namespace concretions address entities by coordinate (the raw coordinate the view projects from). +- ADR 225 — Three-layer extensibility for pack-contributed entity kinds (the `entries` dictionary the view reads). +- ADR 223 — Target-owned default namespace (the default-namespace sentinel the view unwraps). +- [`docs/architecture docs/patterns/interface-plus-factory.md`](../patterns/interface-plus-factory.md) — the `from` / `fromJson` factory shape.