@skmdev/prisma-fixtures 0.1.1 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # @skmdev/prisma-fixtures
2
2
 
3
3
  Load YAML or JSON fixtures into Prisma using named references, generated data and
4
- processors. The CLI can initialize a generated Prisma 7 client from your Prisma
5
- config; the API also accepts a client you create.
6
-
7
- Requires Node.js 22.18+; tested with Prisma 7.10 and PostgreSQL. Prisma itself is
8
- provided by your application, not installed as a runtime dependency of this package.
9
-
10
- - [Quick start](#quick-start)
4
+ processors. The CLI supports generated Prisma 7 clients and emitted Prisma 8
5
+ contracts; the API also accepts a client you create.
6
+
7
+ Requires Node.js 22.18+. The retained path uses Prisma 7.10; the Prisma 8 release
8
+ candidate targets `prisma@8.0.0-rc.17`,
9
+ `@prisma/orm-postgres@8.0.0-rc.12` and PostgreSQL 17+. Prisma itself is provided
10
+ by your application, not installed as a runtime dependency of this package.
11
+ Install `@skmdev/prisma-fixtures@1.0.0-rc.1` after it is published to npm `next`,
12
+ or test it before publication through the local tarball workflow below.
13
+
14
+ - [Prisma 7 quick start](#prisma-7-quick-start)
15
+ - [Prisma 8 migration preview](#prisma-8-migration-preview)
11
16
  - [Fixture format](#fixture-format)
12
17
  - [Processors](#processors)
13
18
  - [API and persistence](#api-and-persistence)
@@ -17,7 +22,7 @@ provided by your application, not installed as a runtime dependency of this pack
17
22
  - [Compatibility and attribution](#compatibility-and-attribution)
18
23
  - [Contributing and releases](CONTRIBUTING.md)
19
24
 
20
- ## Quick start
25
+ ## Prisma 7 quick start
21
26
 
22
27
  Start with an existing Prisma 7 application that has:
23
28
 
@@ -127,11 +132,141 @@ Migration history and tables preserved by `preserveTables` or Prisma's
127
132
  See the [framework and runtime examples](examples/README.md) for standalone apps
128
133
  with Argon2 password processors and API endpoints. NestJS demonstrates a CommonJS client.
129
134
 
135
+ ## Prisma 8 migration preview
136
+
137
+ Prisma 8 support in this checkout uses separately versioned release candidates:
138
+
139
+ ```sh
140
+ npm install @prisma/orm-postgres@8.0.0-rc.12
141
+ npm install --save-dev prisma@8.0.0-rc.17
142
+ ```
143
+
144
+ After the fixture RC is published, install its exact version:
145
+
146
+ ```sh
147
+ npm install --save-dev @skmdev/prisma-fixtures@1.0.0-rc.1
148
+ ```
149
+
150
+ `npm install --save-dev @skmdev/prisma-fixtures@next` discovers the current
151
+ prerelease, while the exact version gives reproducible installs. Before
152
+ `1.0.0-rc.1` is published, build the fixture package from this repository. Its
153
+ baseline package version, and therefore the tarball name, remains `0.1.1` until
154
+ the Prepare release action creates the version-bump pull request:
155
+
156
+ ```sh
157
+ # In this repository
158
+ npm ci
159
+ npm pack
160
+
161
+ # In the Prisma application
162
+ npm install --no-save /path/to/skmdev-prisma-fixtures-1.0.0-rc.1.tgz
163
+ ```
164
+
165
+ An existing multi-file Prisma 7 schema can be the source for an emitted Prisma 8
166
+ contract. Configure `prisma.config.ts`:
167
+
168
+ ```ts
169
+ import { defineConfig, prisma7Schema } from '@prisma/orm-postgres/config'
170
+ import { definePrismaConfig } from 'prisma/config'
171
+
172
+ export default definePrismaConfig({
173
+ orm: defineConfig({
174
+ contract: prisma7Schema('./prisma/schema'),
175
+ output: './src/generated/prisma',
176
+ db: { connection: process.env.DATABASE_URL },
177
+ }),
178
+ })
179
+ ```
180
+
181
+ Emit `contract.json` and `contract.d.ts`, then derive the fixture JSON Schema
182
+ offline from that contract:
183
+
184
+ ```sh
185
+ npx prisma contract emit
186
+ npx prisma-fixtures-generator src/generated/prisma/contract.json src/generated/prisma-fixtures
187
+ npx prisma-fixtures --lint --schema src/generated/prisma-fixtures/schema.json
188
+ ```
189
+
190
+ After creating the database schema with `prisma db init` or your migration
191
+ workflow, load or reset fixtures directly:
192
+
193
+ ```sh
194
+ npx prisma-fixtures
195
+ npx prisma-fixtures --reset
196
+ ```
197
+
198
+ With no `client` in `.prisma-fixtures`, the CLI reads the v8 config, loads the
199
+ emitted contract and constructs the PostgreSQL fixture adapter. To select it
200
+ explicitly, point the existing configured-client shape at `contract.json`:
201
+
202
+ ```json
203
+ {
204
+ "fixtures": ["./fixtures"],
205
+ "client": {
206
+ "module": "./src/generated/prisma/contract.json",
207
+ "adapter": "pg"
208
+ }
209
+ }
210
+ ```
211
+
212
+ Application routes should use the native Prisma 8 `db.orm` API. Only fixture
213
+ loading needs the compatibility bridge:
214
+
215
+ ```ts
216
+ import postgres from '@prisma/orm-postgres/runtime'
217
+ import {
218
+ createPrisma8FixtureClient,
219
+ PrismaFixtures,
220
+ } from '@skmdev/prisma-fixtures'
221
+ import type { Contract } from './src/generated/prisma/contract.js'
222
+ import contractJson from './src/generated/prisma/contract.json' with { type: 'json' }
223
+
224
+ const db = postgres<Contract>({
225
+ contractJson,
226
+ url: process.env.DATABASE_URL!,
227
+ })
228
+ const client = createPrisma8FixtureClient(db)
229
+
230
+ try {
231
+ await new PrismaFixtures().load(client)
232
+ } finally {
233
+ await client.$disconnect()
234
+ }
235
+ ```
236
+
237
+ `createPrisma8FixtureClient(db)` exposes the legacy fixture-facing delegates,
238
+ `$transaction` and `$disconnect` on top of the native v8 client. Prisma 8 fixture
239
+ transactions set PostgreSQL's `transaction_timeout`, so this path requires
240
+ PostgreSQL 17 or newer and retains rollback when the configured timeout expires.
241
+ Enable TypeScript's `resolveJsonModule`; use `contract.js` with NodeNext ESM or
242
+ `contract.d` with bundler module resolution for the generated `Contract` type.
243
+
244
+ The v8 bridge decodes scalar fixture values with the contract's PostgreSQL codecs,
245
+ including nested creates, connections and scalar lists. Quote large integers and
246
+ decimals to preserve precision; use base64 strings for binary fields. JSON fields
247
+ remain JSON. Dates use the emitted codec's ISO format: `Temporal.Instant` takes
248
+ a timezone such as `2026-01-02T03:04:05Z`, while `Temporal.PlainDateTime` takes
249
+ `2026-01-02T03:04:05` without a timezone. Processors can also supply the codec's
250
+ native values directly.
251
+
252
+ If your contract uses Temporal codecs and your runtime has no `Temporal`, install
253
+ the application's polyfill and preload it before constructing the client:
254
+
255
+ ```sh
256
+ npm install temporal-polyfill@1.0.5
257
+ npx prisma-fixtures --require temporal-polyfill/full/global
258
+ ```
259
+
260
+ For programmatic loading, use `import 'temporal-polyfill/full/global'` before
261
+ creating `db`. The fixture package does not install a global polyfill itself.
262
+
130
263
  ## Fixture format
131
264
 
132
- Each `.yml`, `.yaml` or `.json` file describes one entity. `User` and `user` both
133
- resolve to the `user` delegate. Directory files are sorted; reference dependencies
134
- determine insert order. Fixture names must be unique across all files.
265
+ Each `.yml`, `.yaml` or `.json` file describes one entity. Prisma 8 accepts
266
+ namespace-qualified entities such as `public.User`; `User` and `user` are also
267
+ available when that model name is unambiguous across namespaces. Prisma 7 resolves
268
+ `User` and `user` to the `user` delegate. Directory files are sorted; reference
269
+ dependencies determine insert order. Fixture names must be unique across all files.
135
270
 
136
271
  | Feature | Example | Meaning |
137
272
  | ------------------------ | -------------------------------- | ------------------------------------------------------------- |
@@ -346,6 +481,9 @@ seed script, use `try`/`finally` as in the runnable example above.
346
481
  then loads the fixtures and returns the same record map as `loadFixtures`.
347
482
  `FixtureResetOptions` combines `seed`, `refDate` and `preserveTables`. Passing
348
483
  the writer as the third argument remains supported.
484
+ - `createPrisma8FixtureClient(db)` adapts a native Prisma 8 PostgreSQL client to
485
+ the fixture delegate, transaction and disconnect interface. Application queries
486
+ continue to use the original native `db`.
349
487
  - Package-owned failures are `FixtureError` instances with a stable `code`, safe
350
488
  `stage`/source/fixture/path context and the original private `cause` for API
351
489
  callers. Opaque provider, hook and database messages are not copied into the
@@ -372,7 +510,8 @@ const records = await prisma.$transaction(
372
510
  Cleanup currently supports **PostgreSQL only** and requires `$executeRawUnsafe`.
373
511
  It truncates all ordinary/partitioned tables across all non-system schemas in the
374
512
  connected database, including implicit join tables. Tables, schema, indexes,
375
- constraints, sequence values and `_prisma_migrations` records are preserved.
513
+ constraints, sequence values, `_prisma_migrations` records and Prisma 8's
514
+ `prisma_contract` schema are preserved.
376
515
  Each `preserveTables` entry names one existing table as `schema.table`;
377
516
  its data and sequence state are also preserved. Unknown tables and
378
517
  duplicate or malformed entries fail before truncation.
@@ -399,17 +538,17 @@ applied. Rollback cannot undo external side effects in templates or processors.
399
538
 
400
539
  Run these commands from your project directory:
401
540
 
402
- | Command | Purpose |
403
- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
404
- | `npx prisma-fixtures init` | Create config and fixture directory without overwriting an existing config |
405
- | `npx prisma-fixtures` | Load configured fixtures; equivalent to the Quick start's `npx prisma db seed` |
406
- | `npx prisma-fixtures ./fixtures --list` | List fixture names and entities without executing code |
407
- | `npx prisma-fixtures --lint` | Check fixtures without a client or database |
408
- | `npx prisma-fixtures --clean` | Clear database data without reading fixtures (PostgreSQL only) |
409
- | `npx prisma-fixtures --reset` | Clear database data, then load fixtures (PostgreSQL only) |
410
- | `npx prisma-fixtures --seed 42 --refDate 2026-01-01T00:00:00.000Z` | Reproduce generated values and random references |
411
- | `npx prisma-fixtures --config ./test/.prisma-fixtures` | Use a different config file |
412
- | `npx prisma-fixtures --help` | Show all options |
541
+ | Command | Purpose |
542
+ | ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
543
+ | `npx prisma-fixtures init` | Create config and fixture directory without overwriting an existing config |
544
+ | `npx prisma-fixtures` | Load configured fixtures; used directly by v8 and by the v7 seed command |
545
+ | `npx prisma-fixtures ./fixtures --list` | List fixture names and entities without executing code |
546
+ | `npx prisma-fixtures --lint` | Check fixtures without a client or database |
547
+ | `npx prisma-fixtures --clean` | Clear database data without reading fixtures (PostgreSQL only) |
548
+ | `npx prisma-fixtures --reset` | Clear database data, then load fixtures (PostgreSQL only) |
549
+ | `npx prisma-fixtures --seed 42 --refDate 2026-01-01T00:00:00.000Z` | Reproduce generated values and random references |
550
+ | `npx prisma-fixtures --config ./test/.prisma-fixtures` | Use a different config file |
551
+ | `npx prisma-fixtures --help` | Show all options |
413
552
 
414
553
  Use only one of `--clean`, `--reset`, `--list` and `--lint` at a time. Clean and
415
554
  reset affect tables beyond those in your fixtures; see [cleanup and reset](#cleanup-and-reset).
@@ -420,8 +559,8 @@ To reuse an existing directory during setup, run
420
559
 
421
560
  ### Configuration
422
561
 
423
- `.prisma-fixtures` is JSON. Start with the config from Quick start and add only
424
- the options you need:
562
+ `.prisma-fixtures` is JSON. Start with the config from the Prisma 7 quick start
563
+ or Prisma 8 preview and add only the options you need:
425
564
 
426
565
  ```json
427
566
  {
@@ -448,28 +587,32 @@ positional paths and CLI flags are relative to the working directory and overrid
448
587
  matching config fields. Multiple fixture paths form one load and one transaction.
449
588
  Discovery checks only the current directory; an explicit `--config` file must exist.
450
589
 
451
- When `client` is omitted, `--clean` and `--reset` also preserve the PostgreSQL
452
- tables listed in Prisma's `tables.external` (with
453
- `experimental.externalTables` enabled). These `schema.table` entries are combined
454
- with any explicit `preserveTables` entries. For example,
455
- `tables: { external: ['public.flyway_schema_history'] }` in `prisma.config.ts`
456
- preserves that table without repeating it in `.prisma-fixtures`. Direct API calls
457
- still use only the options passed to them. Ordinary loading ignores the preserve
458
- list and never deletes data.
590
+ When `client` is omitted, `--clean` and `--reset` also preserve Prisma 7
591
+ `tables.external` entries from the adjacent config when
592
+ `experimental.externalTables` is enabled. Every Prisma 8 adapter preserves emitted
593
+ contract tables whose control is `external`, `observed` or `tolerated`. This applies
594
+ to CLI clients constructed from inferred or explicit `contract.json` paths and to
595
+ programmatic transactions created by `createPrisma8FixtureClient(db)`. These tables
596
+ are combined with explicit `preserveTables` entries. Ordinary loading ignores the
597
+ preserve list and never deletes data.
459
598
 
460
599
  ### Client discovery and overrides
461
600
 
462
601
  For write commands with no explicit `client`, the CLI reads the adjacent Prisma
463
- config's schema and datasource, resolves the static `prisma-client` output path,
464
- and uses your installed `@prisma/adapter-pg`. Run `prisma generate` first.
465
- For inferred `.ts`/`.cts` clients, it uses existing compiled output described by
466
- `tsconfig.json` with `rootDir` and `outDir`, or the project's installed `ts-node`
467
- when no loader is registered. Native ESM `.mts` clients work with Node's type
468
- stripping; see the [Hono generator](examples/hono/prisma/schema/00_base.prisma).
602
+ config. A Prisma 8 `definePrismaConfig({ orm: ... })` config resolves its emitted
603
+ `contract.json` output and constructs `createPrisma8FixtureClient(db)` with the
604
+ installed `@prisma/orm-postgres` runtime. Run `prisma contract emit` first.
469
605
 
470
- An explicit `client` can be an object with `module` (generated client path),
471
- `adapter: 'pg'` and optional `guard`, or a legacy module path exporting a client
472
- or factory. `--client` overrides either form with a legacy module path.
606
+ The retained Prisma 7 path resolves the static `prisma-client` output and uses
607
+ the installed `@prisma/adapter-pg`; run `prisma generate` first. For inferred
608
+ `.ts`/`.cts` v7 clients, the CLI uses compiled output described by `tsconfig.json`
609
+ or the project's installed `ts-node`. Native ESM `.mts` clients work with Node's
610
+ type stripping.
611
+
612
+ An explicit `client` object uses `module`, `adapter: 'pg'` and an optional
613
+ `guard`. `module` can name a v8 `contract.json` or a generated v7 client. A legacy
614
+ module path exporting a client or factory remains supported, and `--client`
615
+ overrides either configured form with that path.
473
616
 
474
617
  | CLI-only option | Purpose |
475
618
  | ---------------------------------- | ------------------------------------------------------------------------------------ |
@@ -493,6 +636,8 @@ in the application's transaction guard.
493
636
 
494
637
  The CLI wraps writes in one transaction (60-second timeout; override with
495
638
  `--timeout <ms>`) and always disconnects a successfully acquired valid client.
639
+ Prisma 8 enforces this with PostgreSQL 17+'s `transaction_timeout`; Prisma 7 uses
640
+ its client transaction timeout option.
496
641
  `--list` and `--lint` skip all hooks, Prisma config loading and client imports;
497
642
  `--list` only parses the JSON config and fixture documents.
498
643
  Package diagnostics include a stable code and available filename, fixture name
@@ -527,7 +672,20 @@ such as Prettier.
527
672
 
528
673
  ### Generate a schema from your Prisma models
529
674
 
530
- Add a generator to `prisma/schema.prisma`:
675
+ For Prisma 8, emit the contract and pass it to the generator's offline contract
676
+ mode:
677
+
678
+ ```sh
679
+ npx prisma contract emit
680
+ npx prisma-fixtures-generator src/generated/prisma/contract.json src/generated/prisma-fixtures
681
+ ```
682
+
683
+ This writes `src/generated/prisma-fixtures/schema.json`. Contract validation
684
+ fails closed: dictionary fields and unknown codec IDs, including unsupported
685
+ extension-provided codecs, stop generation instead of producing a permissive or
686
+ incorrect schema.
687
+
688
+ For Prisma 7, add the legacy generator block to `prisma/schema.prisma`:
531
689
 
532
690
  ```prisma
533
691
  generator fixtures {
@@ -538,8 +696,8 @@ generator fixtures {
538
696
 
539
697
  Run `npx prisma generate`. This creates `generated/fixtures/schema.json` from
540
698
  Prisma's model and create-input metadata. Output paths are relative to
541
- `schema.prisma`; regenerate after changing your models. No database connection
542
- is needed for this generator.
699
+ `schema.prisma`; regenerate after changing your models. Neither generator mode
700
+ needs a database connection.
543
701
 
544
702
  Use the same schema for CLI lint by adding `schema` to `.prisma-fixtures`:
545
703
 
@@ -556,12 +714,15 @@ Or pass it explicitly:
556
714
  npx prisma-fixtures ./fixtures --lint --schema ./generated/fixtures/schema.json
557
715
  ```
558
716
 
559
- The generated schema selects fields by `entity`, accepts model and delegate
560
- names, and checks known fields, scalar types, enums, nullability, lists and
561
- required create inputs. Both nested relation inputs and direct foreign keys are
562
- supported. References, Faker, EJS and parameter expressions remain dynamic
563
- strings; their evaluated types are checked when loading. References and records
564
- with an `id` (including arrays) are allowed for `connectedFields` shorthand.
717
+ The generated schema selects fields by `entity`. Prisma 8 schemas include
718
+ namespace-qualified names such as `public.User`, plus unqualified model and
719
+ delegate aliases when they are unambiguous; Prisma 7 schemas accept model and
720
+ delegate names. The schema checks known fields, scalar types, enums, nullability,
721
+ lists and required create inputs. Both nested relation inputs and direct foreign
722
+ keys are supported. References, Faker, EJS and parameter expressions remain
723
+ dynamic strings; their evaluated types are checked when loading. References and
724
+ records with an `id` (including arrays) are allowed for `connectedFields`
725
+ shorthand.
565
726
 
566
727
  Documents with a `processor` skip model item validation because the processor
567
728
  may add, remove or transform fields; their fixture structure is still checked.
@@ -616,6 +777,13 @@ visited nodes per validated value. No fixture values are logged by the engine.
616
777
 
617
778
  ## Compatibility and attribution
618
779
 
780
+ The Prisma 8 release candidate is tested against the exact RC pair documented
781
+ above and PostgreSQL 17+. Prisma 8 CLI and runtime packages have separate version
782
+ numbers. Prisma 7.10 support, its generator RPC and existing client injection
783
+ remain available for legacy consumers. A stable fixture package `1.0.0` requires
784
+ stable Prisma 8 CLI and runtime releases and successful migration verification
785
+ against those stable versions.
786
+
619
787
  Legacy Faker aliases `name.firstName`, `name.lastName`, `name.title`,
620
788
  `internet.userName` and `random.number` remain supported. This is syntax
621
789
  compatibility, not identical random output or complete emulation of every removed
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ const load_options_1 = require("./load-options");
17
17
  const prisma_config_1 = require("./prisma-config");
18
18
  const fixture_config_1 = require("./fixture-config");
19
19
  const index_1 = require("./index");
20
+ const prisma8_client_1 = require("./prisma8-client");
20
21
  const help = `Usage: prisma-fixtures [path...] [--client <module>] [options]
21
22
  prisma-fixtures init [fixtures-directory]
22
23
 
@@ -276,8 +277,16 @@ async function loadClient(config, values) {
276
277
  if (typeof databaseUrl !== 'string' || !databaseUrl.trim()) {
277
278
  throw new Error('DATABASE_URL is required');
278
279
  }
279
- const generated = await importGeneratedClient(clientConfig.module, requireFromCwd);
280
- candidate = createGeneratedClient(generated, requireFromCwd, databaseUrl);
280
+ if (node_path_1.default.extname(clientConfig.module) === '.json') {
281
+ const { default: postgres } = (await import((0, node_url_1.pathToFileURL)(requireFromCwd.resolve('@prisma/orm-postgres/runtime'))
282
+ .href));
283
+ const contractJson = JSON.parse(node_fs_1.default.readFileSync(clientConfig.module, 'utf8'));
284
+ candidate = (0, prisma8_client_1.createPrisma8FixtureClient)(postgres({ contractJson, url: databaseUrl }));
285
+ }
286
+ else {
287
+ const generated = await importGeneratedClient(clientConfig.module, requireFromCwd);
288
+ candidate = createGeneratedClient(generated, requireFromCwd, databaseUrl);
289
+ }
281
290
  }
282
291
  if (!candidate ||
283
292
  typeof candidate !== 'object' ||
@@ -0,0 +1,8 @@
1
+ type JsonSchema = Record<string, unknown>;
2
+ type FixtureSchema = JsonSchema & {
3
+ properties: Record<string, JsonSchema>;
4
+ definitions: Record<string, JsonSchema>;
5
+ allOf?: JsonSchema[];
6
+ };
7
+ export declare function buildContractFixtureSchema(value: unknown): FixtureSchema;
8
+ export {};
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildContractFixtureSchema = buildContractFixtureSchema;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const baseFixtureSchema = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, '../schema/fixture.schema.json'), 'utf8'));
10
+ const dynamicString = () => ({
11
+ type: 'string',
12
+ pattern: '(?:^@(?!@)|<%|\\{\\{|<\\{|\\(\\$current(?:[+*/-][0-9]+)?\\))',
13
+ });
14
+ const internalRef = (name) => ({
15
+ $ref: `#/definitions/${encodeURIComponent(name.replaceAll('~', '~0').replaceAll('/', '~1'))}`,
16
+ });
17
+ const invalid = (message) => {
18
+ throw new Error(`Invalid Prisma v8 PostgreSQL contract: ${message}`);
19
+ };
20
+ const object = (value, location) => {
21
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
22
+ invalid(`${location} must be an object`);
23
+ return value;
24
+ };
25
+ const text = (value, location) => {
26
+ if (typeof value !== 'string' || !value)
27
+ invalid(`${location} must be a non-empty string`);
28
+ return value;
29
+ };
30
+ function buildContractFixtureSchema(value) {
31
+ const contract = contractMetadata(value);
32
+ const schema = structuredClone(baseFixtureSchema);
33
+ const definitions = schema.definitions;
34
+ const aliasOwners = new Map();
35
+ for (const [namespace, entry] of Object.entries(contract.domain.namespaces)) {
36
+ for (const model of Object.keys(object(entry.models, `${namespace}.models`))) {
37
+ const owner = `${namespace}.${model}`;
38
+ for (const alias of new Set([
39
+ owner,
40
+ model,
41
+ model[0].toLowerCase() + model.slice(1),
42
+ ])) {
43
+ aliasOwners.set(alias, aliasOwners.has(alias) && aliasOwners.get(alias) !== owner
44
+ ? null
45
+ : owner);
46
+ }
47
+ }
48
+ }
49
+ const aliases = (namespace, model) => [...aliasOwners]
50
+ .filter(([, owner]) => owner === `${namespace}.${model}`)
51
+ .map(([alias]) => alias);
52
+ const enumValues = (namespace, field) => {
53
+ if (!field.valueSet)
54
+ return undefined;
55
+ const ref = field.valueSet;
56
+ const refNamespace = ref.namespaceId === '__unbound__' ? namespace : ref.namespaceId;
57
+ const values = ref.plane === 'domain' && ref.entityKind === 'enum'
58
+ ? contract.domain.namespaces[refNamespace]?.enum?.[ref.entityName]?.members.map(({ value }) => value)
59
+ : ref.plane === 'storage' && ref.entityKind === 'valueSet'
60
+ ? contract.storage.namespaces[refNamespace]?.entries.valueSet?.[ref.entityName]?.values
61
+ : undefined;
62
+ if (!values)
63
+ invalid(`field value set ${refNamespace}.${ref.entityName} is invalid`);
64
+ return values;
65
+ };
66
+ const typeSchema = (namespace, value) => {
67
+ const type = object(value, 'field type');
68
+ if (type.kind === 'scalar')
69
+ return scalarSchema(text(type.codecId, 'codecId'));
70
+ if (type.kind === 'valueObject') {
71
+ const name = text(type.name, 'value object name');
72
+ const definitionName = `PrismaContractValueObject_${JSON.stringify([namespace, name])}`;
73
+ if (!(definitionName in definitions)) {
74
+ const valueObject = contract.domain.namespaces[namespace]?.valueObjects?.[name] ??
75
+ invalid(`value object ${namespace}.${name} does not exist`);
76
+ definitions[definitionName] = {};
77
+ definitions[definitionName] = objectSchema(namespace, valueObject.fields, {});
78
+ }
79
+ return internalRef(definitionName);
80
+ }
81
+ if (type.kind === 'union' &&
82
+ Array.isArray(type.members) &&
83
+ type.members.length)
84
+ return {
85
+ anyOf: type.members.map((member) => typeSchema(namespace, member)),
86
+ };
87
+ return invalid('field type kind is invalid');
88
+ };
89
+ const fieldSchema = (namespace, field) => {
90
+ if (typeof field.nullable !== 'boolean')
91
+ invalid('field nullable must be boolean');
92
+ if (field.many !== undefined && field.many !== true)
93
+ invalid('field many must be true when present');
94
+ if (field.dict)
95
+ invalid('dictionary fields are not supported');
96
+ const values = enumValues(namespace, field);
97
+ const value = values ? { enum: values } : typeSchema(namespace, field.type);
98
+ let result = field.many
99
+ ? { type: 'array', items: { anyOf: [value, dynamicString()] } }
100
+ : value;
101
+ const acceptsString = values === undefined &&
102
+ !field.many &&
103
+ field.type.kind === 'scalar' &&
104
+ (stringCodec(field.type.codecId) || jsonCodec(field.type.codecId));
105
+ if (!acceptsString)
106
+ result = { anyOf: [result, dynamicString()] };
107
+ if (field.nullable)
108
+ result = { anyOf: [result, { type: 'null' }] };
109
+ return result;
110
+ };
111
+ const connection = () => ({
112
+ allOf: [
113
+ internalRef('safeObject'),
114
+ {
115
+ minProperties: 1,
116
+ propertyNames: { not: { enum: ['connect', 'create'] } },
117
+ },
118
+ ],
119
+ });
120
+ const ensureModel = (namespace, name) => {
121
+ const definitionName = `PrismaContractModel_${JSON.stringify([namespace, name])}`;
122
+ if (definitionName in definitions)
123
+ return definitionName;
124
+ const model = contract.domain.namespaces[namespace]?.models[name];
125
+ if (!model)
126
+ invalid(`relation target ${namespace}.${name} does not exist`);
127
+ definitions[definitionName] = {};
128
+ definitions[definitionName] = objectSchema(namespace, model.fields, model.relations, model, name);
129
+ return definitionName;
130
+ };
131
+ const ensureNestedModel = (sourceNamespace, sourceName, sourceModel, relationName, relation) => {
132
+ const namespace = text(relation.to?.namespace, 'relation namespace');
133
+ const name = text(relation.to?.model, 'relation model');
134
+ const omitted = parentInjectedFields(contract, sourceNamespace, sourceName, sourceModel, relation);
135
+ if (!omitted.length)
136
+ return ensureModel(namespace, name);
137
+ const definitionName = `PrismaContractNested_${JSON.stringify([sourceNamespace, sourceName, relationName])}`;
138
+ if (definitionName in definitions)
139
+ return definitionName;
140
+ const model = contract.domain.namespaces[namespace]?.models[name];
141
+ if (!model)
142
+ invalid(`relation target ${namespace}.${name} does not exist`);
143
+ definitions[definitionName] = {};
144
+ definitions[definitionName] = objectSchema(namespace, model.fields, model.relations, model, name, new Set(omitted));
145
+ return definitionName;
146
+ };
147
+ const relationSchema = (namespace, modelName, model, relationName, relation) => {
148
+ if (!['1:1', 'N:1', '1:N', 'N:M'].includes(relation.cardinality))
149
+ invalid('relation cardinality is invalid');
150
+ const target = internalRef(model && modelName
151
+ ? ensureNestedModel(namespace, modelName, model, relationName, relation)
152
+ : ensureModel(text(relation.to?.namespace, 'relation namespace'), text(relation.to?.model, 'relation model')));
153
+ const oneOrMany = (item) => ({
154
+ anyOf: [
155
+ item,
156
+ dynamicString(),
157
+ { type: 'array', items: { anyOf: [item, dynamicString()] } },
158
+ ],
159
+ });
160
+ const selector = connection();
161
+ const alternatives = [
162
+ dynamicString(),
163
+ selector,
164
+ {
165
+ type: 'object',
166
+ additionalProperties: false,
167
+ properties: {
168
+ connect: oneOrMany(selector),
169
+ create: oneOrMany(target),
170
+ },
171
+ oneOf: [{ required: ['connect'] }, { required: ['create'] }],
172
+ },
173
+ ];
174
+ if (relation.cardinality === '1:N' || relation.cardinality === 'N:M')
175
+ alternatives.push({
176
+ type: 'array',
177
+ items: { anyOf: [dynamicString(), selector] },
178
+ });
179
+ if (relation.nullable)
180
+ alternatives.push({ type: 'null' });
181
+ return { anyOf: alternatives };
182
+ };
183
+ function objectSchema(namespace, fields, relations, model, modelName, omittedRequiredFields = new Set()) {
184
+ const properties = Object.fromEntries(Object.entries(object(fields, `${namespace} fields`)).map(([name, field]) => [name, fieldSchema(namespace, field)]));
185
+ const required = Object.entries(fields)
186
+ .filter(([name, field]) => !field.nullable &&
187
+ !omittedRequiredFields.has(name) &&
188
+ !hasDefault(contract, model, name))
189
+ .map(([name]) => name);
190
+ const relationRequirements = [];
191
+ for (const [name, relation] of Object.entries(object(relations, `${namespace} relations`))) {
192
+ properties[name] = relationSchema(namespace, modelName, model, name, relation);
193
+ if (relation.nullable === false &&
194
+ (relation.cardinality === 'N:1' ||
195
+ (relation.cardinality === '1:1' &&
196
+ ownsForeignKey(contract, model, relation)))) {
197
+ const local = relation.on?.localFields.filter((field) => required.includes(field));
198
+ if (local?.length) {
199
+ for (const field of local)
200
+ required.splice(required.indexOf(field), 1);
201
+ relationRequirements.push({
202
+ anyOf: [{ required: local }, { required: [name] }],
203
+ });
204
+ }
205
+ }
206
+ }
207
+ return {
208
+ type: 'object',
209
+ additionalProperties: false,
210
+ properties,
211
+ ...(required.length ? { required } : {}),
212
+ ...(relationRequirements.length ? { allOf: relationRequirements } : {}),
213
+ };
214
+ }
215
+ const entityNames = [];
216
+ const conditions = [];
217
+ for (const [namespace, entry] of Object.entries(contract.domain.namespaces)) {
218
+ for (const [name, model] of Object.entries(entry.models)) {
219
+ const modelAliases = aliases(namespace, name);
220
+ const relations = Object.keys(model.relations);
221
+ const scalars = Object.keys(model.fields);
222
+ entityNames.push(...modelAliases);
223
+ conditions.push({
224
+ if: {
225
+ required: ['entity'],
226
+ properties: { entity: { enum: modelAliases } },
227
+ },
228
+ then: {
229
+ properties: {
230
+ connectedFields: relations.length
231
+ ? { items: { enum: relations } }
232
+ : { maxItems: 0 },
233
+ deferredFields: scalars.length
234
+ ? { items: { enum: scalars } }
235
+ : { maxItems: 0 },
236
+ },
237
+ allOf: [
238
+ {
239
+ if: { not: { required: ['processor'] } },
240
+ then: {
241
+ properties: {
242
+ items: {
243
+ additionalProperties: internalRef(ensureModel(namespace, name)),
244
+ },
245
+ },
246
+ },
247
+ },
248
+ ],
249
+ },
250
+ });
251
+ }
252
+ }
253
+ schema.properties.entity = entityNames.length
254
+ ? {
255
+ description: 'Prisma v8 namespace-qualified model or an unambiguous model alias.',
256
+ type: 'string',
257
+ enum: [...new Set(entityNames)],
258
+ }
259
+ : { description: 'No Prisma models are available.', not: {} };
260
+ schema.allOf = [...(schema.allOf ?? []), ...conditions];
261
+ return schema;
262
+ }
263
+ function contractMetadata(value) {
264
+ const root = object(value, 'contract');
265
+ if (root.schemaVersion !== '1')
266
+ invalid('schemaVersion must be "1"');
267
+ if (root.targetFamily !== 'sql' || root.target !== 'postgres')
268
+ invalid('contract must target PostgreSQL');
269
+ const domain = object(root.domain, 'domain');
270
+ const storage = object(root.storage, 'storage');
271
+ object(root.roots, 'roots');
272
+ object(domain.namespaces, 'domain.namespaces');
273
+ object(storage.namespaces, 'storage.namespaces');
274
+ return root;
275
+ }
276
+ function hasDefault(contract, model, name) {
277
+ if (!model)
278
+ return false;
279
+ const field = model.storage.fields[name];
280
+ if (!field)
281
+ return false;
282
+ const column = contract.storage.namespaces[model.storage.namespaceId]?.entries.table[model.storage.table]?.columns[field.column];
283
+ return ((column !== undefined && 'default' in column) ||
284
+ (contract.execution?.mutations.defaults.some(({ ref, onCreate }) => onCreate !== undefined &&
285
+ ref.namespace === model.storage.namespaceId &&
286
+ ref.table === model.storage.table &&
287
+ ref.column === field.column) ??
288
+ false));
289
+ }
290
+ function parentInjectedFields(contract, sourceNamespace, sourceName, sourceModel, relation) {
291
+ if (!relation.on ||
292
+ (relation.cardinality !== '1:N' && relation.cardinality !== '1:1'))
293
+ return [];
294
+ const target = contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model];
295
+ if (!target ||
296
+ !relation.on.localFields.every((field) => field in sourceModel.fields) ||
297
+ !relation.on.targetFields.every((field) => field in target.fields))
298
+ return [];
299
+ const inverse = Object.values(target.relations).find((candidate) => candidate.to.namespace === sourceNamespace &&
300
+ candidate.to.model === sourceName &&
301
+ candidate.on !== undefined &&
302
+ sameFields(candidate.on.localFields, relation.on.targetFields) &&
303
+ sameFields(candidate.on.targetFields, relation.on.localFields) &&
304
+ ownsForeignKey(contract, target, candidate));
305
+ return inverse ? relation.on.targetFields : [];
306
+ }
307
+ function sameFields(left, right) {
308
+ return (left.length === right.length &&
309
+ left.every((field, index) => field === right[index]));
310
+ }
311
+ function ownsForeignKey(contract, model, relation) {
312
+ if (!model || !relation.on)
313
+ return false;
314
+ const sourceColumns = relation.on.localFields.map((field) => model.storage.fields[field]?.column);
315
+ const target = contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model];
316
+ const targetColumns = relation.on.targetFields.map((field) => target?.storage.fields[field]?.column);
317
+ if (!target ||
318
+ sourceColumns.some((column) => column === undefined) ||
319
+ targetColumns.some((column) => column === undefined))
320
+ return false;
321
+ const table = contract.storage.namespaces[model.storage.namespaceId]?.entries.table[model.storage.table];
322
+ return table?.foreignKeys.some(({ source, target: foreignTarget }) => source.namespaceId === model.storage.namespaceId &&
323
+ source.tableName === model.storage.table &&
324
+ sameFields(source.columns, sourceColumns) &&
325
+ foreignTarget.namespaceId === target.storage.namespaceId &&
326
+ foreignTarget.tableName === target.storage.table &&
327
+ sameFields(foreignTarget.columns, targetColumns));
328
+ }
329
+ const jsonCodec = (codecId) => codecId === 'pg/json@1' || codecId === 'pg/jsonb@1';
330
+ const stringCodec = (codecId) => /^(?:pg\/(?:text|enum|char|varchar|bit|varbit|date-(?:string|temporal)|timestamp-(?:string|temporal)|timestamptz-(?:string|temporal|date)|time-(?:string|temporal)|timetz|interval|bytea|uuid|inet|tsquery)|sql\/(?:char|varchar|text))@1$/.test(codecId);
331
+ const scalarSchema = (codecId) => {
332
+ if (jsonCodec(codecId))
333
+ return internalRef('jsonValue');
334
+ if (codecId === 'pg/bool@1')
335
+ return { type: 'boolean' };
336
+ if (/^(?:pg\/(?:int|int2|int4|int8number)|sql\/int)@1$/.test(codecId))
337
+ return { type: 'integer' };
338
+ if (/^pg\/(?:int8|unboundedint)@1$/.test(codecId))
339
+ return {
340
+ anyOf: [{ type: 'integer' }, { type: 'string', pattern: '^-?[0-9]+$' }],
341
+ };
342
+ if (/^(?:pg\/(?:float|float4|float8)|sql\/float)@1$/.test(codecId))
343
+ return { type: 'number' };
344
+ if (codecId === 'pg/numeric@1')
345
+ return { anyOf: [{ type: 'number' }, { type: 'string' }] };
346
+ if (codecId === 'pg/text-array@1')
347
+ return { type: 'array', items: { type: 'string' } };
348
+ if (stringCodec(codecId))
349
+ return { type: 'string' };
350
+ return invalid(`field type codec ${codecId} is not supported`);
351
+ };
@@ -295,8 +295,10 @@ function assertFixtureDefinition(value) {
295
295
  function assertFixtureMetadata(metadata, file) {
296
296
  const { entity, parameters, processor, locale, connectedFields, deferredFields, } = metadata;
297
297
  if (typeof entity !== 'string' ||
298
- !NAME_PATTERN.test(entity) ||
299
- exports.DANGEROUS_KEYS.has(entity) ||
298
+ entity.split('.').length > 2 ||
299
+ entity
300
+ .split('.')
301
+ .some((part) => !NAME_PATTERN.test(part) || exports.DANGEROUS_KEYS.has(part)) ||
300
302
  !isFixtureRecord(parameters) ||
301
303
  (processor !== undefined &&
302
304
  (typeof processor !== 'string' || !processor)) ||
package/dist/generator.js CHANGED
@@ -5,21 +5,42 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_module_1 = require("node:module");
8
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const node_url_1 = require("node:url");
9
11
  const generator_helper_1 = require("@prisma/generator-helper");
12
+ const contract_fixture_schema_1 = require("./contract-fixture-schema");
10
13
  const fixture_schema_1 = require("./fixture-schema");
11
- (0, generator_helper_1.generatorHandler)({
12
- onManifest() {
13
- return {
14
- prettyName: 'Prisma Fixtures',
15
- defaultOutput: './generated/fixtures',
16
- };
17
- },
18
- async onGenerate(options) {
19
- const output = options.generator.output?.value;
20
- if (!output)
21
- throw new Error('Prisma Fixtures generator requires an output path');
22
- node_fs_1.default.mkdirSync(output, { recursive: true });
23
- node_fs_1.default.writeFileSync(node_path_1.default.join(output, 'schema.json'), `${JSON.stringify((0, fixture_schema_1.buildFixtureSchema)(options.dmmf), null, 2)}\n`);
24
- },
25
- });
14
+ const writeSchema = (output, schema) => {
15
+ node_fs_1.default.mkdirSync(output, { recursive: true });
16
+ node_fs_1.default.writeFileSync(node_path_1.default.join(output, 'schema.json'), `${JSON.stringify(schema, null, 2)}\n`);
17
+ };
18
+ const args = process.argv.slice(2);
19
+ if (args.length) {
20
+ if (args.length !== 2)
21
+ throw new Error('Usage: prisma-fixtures-generator <contract.json> <output-directory>');
22
+ const [contractPath, output] = args;
23
+ const contract = JSON.parse(node_fs_1.default.readFileSync(contractPath, 'utf8'));
24
+ const consumerRequire = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
25
+ const runtime = consumerRequire.resolve('@prisma/orm-postgres/target/runtime');
26
+ void import((0, node_url_1.pathToFileURL)(runtime).href).then(({ PostgresContractSerializer }) => {
27
+ new PostgresContractSerializer().deserializeContract(contract);
28
+ writeSchema(output, (0, contract_fixture_schema_1.buildContractFixtureSchema)(contract));
29
+ });
30
+ }
31
+ else {
32
+ (0, generator_helper_1.generatorHandler)({
33
+ onManifest() {
34
+ return {
35
+ prettyName: 'Prisma Fixtures',
36
+ defaultOutput: './generated/fixtures',
37
+ };
38
+ },
39
+ async onGenerate(options) {
40
+ const output = options.generator.output?.value;
41
+ if (!output)
42
+ throw new Error('Prisma Fixtures generator requires an output path');
43
+ writeSchema(output, (0, fixture_schema_1.buildFixtureSchema)(options.dmmf));
44
+ },
45
+ });
46
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type FixtureDefinition, readFixtureDocuments } from './fixture-document';
2
2
  import { type FixtureCleanupOptions } from './cleanup-options';
3
3
  import { type FixtureLoadOptions, type FixtureResetOptions } from './load-options';
4
+ export { createPrisma8FixtureClient } from './prisma8-client';
4
5
  export type { FixtureDefinition } from './fixture-document';
5
6
  export { FixtureError } from './fixture-error';
6
7
  export type { FixtureErrorCode, FixtureErrorContext } from './fixture-error';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PrismaFixtures = exports.readFixtureDefinitions = exports.FixtureError = void 0;
3
+ exports.PrismaFixtures = exports.readFixtureDefinitions = exports.FixtureError = exports.createPrisma8FixtureClient = void 0;
4
4
  exports.loadFixtures = loadFixtures;
5
5
  exports.cleanFixtures = cleanFixtures;
6
6
  exports.resetFixtures = resetFixtures;
@@ -11,6 +11,9 @@ const cleanup_options_1 = require("./cleanup-options");
11
11
  const load_options_1 = require("./load-options");
12
12
  const fixture_error_1 = require("./fixture-error");
13
13
  const fixture_config_1 = require("./fixture-config");
14
+ const prisma8_client_1 = require("./prisma8-client");
15
+ var prisma8_client_2 = require("./prisma8-client");
16
+ Object.defineProperty(exports, "createPrisma8FixtureClient", { enumerable: true, get: function () { return prisma8_client_2.createPrisma8FixtureClient; } });
14
17
  var fixture_error_2 = require("./fixture-error");
15
18
  Object.defineProperty(exports, "FixtureError", { enumerable: true, get: function () { return fixture_error_2.FixtureError; } });
16
19
  exports.readFixtureDefinitions = fixture_document_1.readFixtureDocuments;
@@ -45,11 +48,17 @@ async function loadFixtures(client, definitions, optionsOrWrite, finalWrite) {
45
48
  return writeFixtures(fixtures, write);
46
49
  }
47
50
  async function cleanFixtures(client, options) {
48
- const { preserveTables = [] } = (0, cleanup_options_1.normalizeCleanupOptions)(options);
51
+ const { preserveTables: requestedTables = [] } = (0, cleanup_options_1.normalizeCleanupOptions)(options);
49
52
  if (client === null ||
50
53
  (typeof client !== 'object' && typeof client !== 'function')) {
51
54
  throw new Error('Invalid fixture cleaner arguments');
52
55
  }
56
+ const preserveTables = [
57
+ ...new Set([
58
+ ...requestedTables,
59
+ ...(prisma8_client_1.prisma8PreservedTables.get(client) ?? []),
60
+ ]),
61
+ ];
53
62
  const execute = client.$executeRawUnsafe;
54
63
  if (typeof execute !== 'function') {
55
64
  throw new Error('Fixture cleaner requires $executeRawUnsafe');
@@ -86,7 +95,7 @@ BEGIN
86
95
  FROM pg_catalog.pg_class AS candidate
87
96
  JOIN pg_catalog.pg_namespace AS namespace ON namespace.oid = candidate.relnamespace
88
97
  WHERE candidate.relkind IN ('r', 'p')
89
- AND namespace.nspname <> 'information_schema'
98
+ AND namespace.nspname NOT IN ('information_schema', 'prisma_contract')
90
99
  AND namespace.nspname !~ '^pg_'
91
100
  AND candidate.relname <> '_prisma_migrations'
92
101
  AND NOT EXISTS (
@@ -115,7 +124,7 @@ BEGIN
115
124
  )
116
125
  INTO tables
117
126
  FROM pg_catalog.pg_tables
118
- WHERE schemaname <> 'information_schema'
127
+ WHERE schemaname NOT IN ('information_schema', 'prisma_contract')
119
128
  AND schemaname !~ '^pg_'
120
129
  AND tablename <> '_prisma_migrations'
121
130
  AND NOT EXISTS (
@@ -7,17 +7,20 @@ exports.loadPrismaDefaults = loadPrismaDefaults;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_module_1 = require("node:module");
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const node_url_1 = require("node:url");
11
+ const prisma8_client_1 = require("./prisma8-client");
10
12
  const cleanup_options_1 = require("./cleanup-options");
11
13
  async function loadPrismaDefaults(configRoot, requireFromCwd, databaseUrlOverride) {
12
14
  const requireFromPrisma = (0, node_module_1.createRequire)(requireFromCwd.resolve('prisma/package.json'));
13
- const { loadConfigFromFile } = requireFromPrisma('@prisma/config');
15
+ const version = requireFromPrisma('./package.json').version;
14
16
  const previousUrl = process.env.DATABASE_URL;
15
17
  if (databaseUrlOverride !== undefined) {
16
18
  process.env.DATABASE_URL = databaseUrlOverride;
17
19
  }
18
- let loaded;
19
20
  try {
20
- loaded = await loadConfigFromFile({ configRoot });
21
+ return version.startsWith('8.')
22
+ ? await loadPrisma8Defaults(configRoot, requireFromPrisma)
23
+ : await loadPrisma7Defaults(configRoot, requireFromPrisma, requireFromCwd);
21
24
  }
22
25
  finally {
23
26
  if (databaseUrlOverride !== undefined) {
@@ -27,6 +30,10 @@ async function loadPrismaDefaults(configRoot, requireFromCwd, databaseUrlOverrid
27
30
  process.env.DATABASE_URL = previousUrl;
28
31
  }
29
32
  }
33
+ }
34
+ async function loadPrisma7Defaults(configRoot, requireFromPrisma, requireFromCwd) {
35
+ const { loadConfigFromFile } = requireFromPrisma('@prisma/config');
36
+ const loaded = await loadConfigFromFile({ configRoot });
30
37
  if (!loaded.resolvedPath || !loaded.config || loaded.error) {
31
38
  throw new Error('Prisma config could not be loaded');
32
39
  }
@@ -40,6 +47,20 @@ async function loadPrismaDefaults(configRoot, requireFromCwd, databaseUrlOverrid
40
47
  }).preserveTables ?? [],
41
48
  };
42
49
  }
50
+ async function loadPrisma8Defaults(configRoot, requireFromPrisma) {
51
+ const { loadConfigForSections } = (await import((0, node_url_1.pathToFileURL)(requireFromPrisma.resolve('@prisma/orm-toolchain/config-loader')).href));
52
+ const loaded = await loadConfigForSections(node_path_1.default.join(configRoot, 'prisma.config.ts'), ['contract', 'db']);
53
+ const config = loaded.ok ? loaded.value : undefined;
54
+ const output = config?.contract?.output;
55
+ if (!output || config?.extensions?.length) {
56
+ throw new Error('Prisma 8 config requires an emitted contract and a supported runtime');
57
+ }
58
+ return {
59
+ module: output,
60
+ databaseUrl: config?.db?.connection,
61
+ preserveTables: (0, prisma8_client_1.contractPreservedTables)(JSON.parse(node_fs_1.default.readFileSync(output, 'utf8'))),
62
+ };
63
+ }
43
64
  function defaultSchema(root) {
44
65
  for (const name of ['prisma/schema.prisma', 'schema.prisma']) {
45
66
  const file = node_path_1.default.join(root, name);
@@ -0,0 +1,21 @@
1
+ type Data = Record<string, unknown>;
2
+ export declare const prisma8PreservedTables: WeakMap<object, string[]>;
3
+ export declare function contractPreservedTables(value: unknown): string[];
4
+ /** Bridge a native Prisma 8 PostgreSQL client to the fixture API. Requires PostgreSQL 17+. */
5
+ export declare function createPrisma8FixtureClient(client: object): Record<string, {
6
+ create(args: {
7
+ data: Data;
8
+ }): Promise<unknown>;
9
+ update(args: {
10
+ where: Data;
11
+ data: Data;
12
+ }): Promise<unknown>;
13
+ }> & {
14
+ $executeRawUnsafe(sql: string): Promise<unknown>;
15
+ } & {
16
+ $disconnect: () => Promise<void>;
17
+ $transaction<T>(action: (tx: object) => Promise<T>, { timeout }?: {
18
+ timeout?: number | undefined;
19
+ }): Promise<T>;
20
+ };
21
+ export {};
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prisma8PreservedTables = void 0;
4
+ exports.contractPreservedTables = contractPreservedTables;
5
+ exports.createPrisma8FixtureClient = createPrisma8FixtureClient;
6
+ const fixture_document_1 = require("./fixture-document");
7
+ const cleanup_options_1 = require("./cleanup-options");
8
+ // Internal cleanup metadata follows the adapted client and each transaction.
9
+ exports.prisma8PreservedTables = new WeakMap();
10
+ function contractPreservedTables(value) {
11
+ const contract = value;
12
+ const names = [];
13
+ for (const [namespace, entry] of Object.entries(contract.storage?.namespaces ?? {})) {
14
+ for (const [table, metadata] of Object.entries(entry.entries?.table ?? {})) {
15
+ if (metadata.control !== undefined && metadata.control !== 'managed')
16
+ names.push(`${namespace}.${table}`);
17
+ }
18
+ }
19
+ return (0, cleanup_options_1.normalizeCleanupOptions)({ preserveTables: names }).preserveTables ?? [];
20
+ }
21
+ /** Bridge a native Prisma 8 PostgreSQL client to the fixture API. Requires PostgreSQL 17+. */
22
+ function createPrisma8FixtureClient(client) {
23
+ const db = client;
24
+ if (!db ||
25
+ !(0, fixture_document_1.isFixtureRecord)(db.contract?.domain?.namespaces) ||
26
+ !(0, fixture_document_1.isFixtureRecord)(db.orm) ||
27
+ typeof db.transaction !== 'function' ||
28
+ typeof db.close !== 'function' ||
29
+ typeof db.runtime !== 'function' ||
30
+ typeof db.raw?.sql !== 'function') {
31
+ throw new Error('Expected a Prisma 8 PostgreSQL client');
32
+ }
33
+ const namespaces = db.contract.domain.namespaces;
34
+ const preserveTables = contractPreservedTables(db.contract);
35
+ const aliases = new Map();
36
+ for (const [namespace, entry] of Object.entries(namespaces)) {
37
+ for (const name of Object.keys(entry.models)) {
38
+ for (const alias of new Set([
39
+ `${namespace}.${name}`,
40
+ name,
41
+ name[0].toLowerCase() + name.slice(1),
42
+ ])) {
43
+ aliases.set(alias, aliases.has(alias) ? null : { namespace, name });
44
+ }
45
+ }
46
+ }
47
+ const plan = (sql) => db.raw
48
+ .sql(Object.assign([sql], { raw: [sql] }))
49
+ .affectedCount()
50
+ .build();
51
+ function scalarValue(model, field, value) {
52
+ const metadata = model.fields?.[field];
53
+ const codecId = metadata?.type.codecId;
54
+ if (metadata?.type.kind !== 'scalar' ||
55
+ !codecId ||
56
+ !/^pg\/(?:int8|unboundedint|int8number|numeric|bytea|interval|(?:date|timestamp|timestamptz|time)-temporal|timestamptz-date)@1$/.test(codecId))
57
+ return value;
58
+ const decode = (item) => {
59
+ // Native objects returned by processors/references already have the runtime type.
60
+ if (typeof item !== 'string' && typeof item !== 'number')
61
+ return item;
62
+ if (typeof item === 'number') {
63
+ if (codecId === 'pg/int8number@1')
64
+ return item;
65
+ if (!/^pg\/(?:int8|unboundedint|numeric)@1$/.test(codecId))
66
+ return item;
67
+ if (!Number.isFinite(item) ||
68
+ (codecId !== 'pg/numeric@1' && !Number.isSafeInteger(item)))
69
+ throw new Error('Invalid numeric fixture value');
70
+ item = String(item);
71
+ }
72
+ const storage = model.storage;
73
+ const column = storage?.fields[field]?.column;
74
+ const codec = storage && column
75
+ ? db.context.contractCodecs.forColumn(storage.namespaceId, storage.table, column)
76
+ : undefined;
77
+ if (!codec)
78
+ throw new Error('Prisma 8 fixture column codec not found');
79
+ return codec.decodeJson(item);
80
+ };
81
+ return metadata.many && Array.isArray(value)
82
+ ? value.map(decode)
83
+ : decode(value);
84
+ }
85
+ function relationData(data, model) {
86
+ return Object.fromEntries(Object.entries(data).map(([field, value]) => {
87
+ const relation = model.relations[field];
88
+ if (!relation)
89
+ return [field, scalarValue(model, field, value)];
90
+ if (!(0, fixture_document_1.isFixtureRecord)(value) ||
91
+ Object.keys(value).length !== 1 ||
92
+ (!Object.hasOwn(value, 'connect') && !Object.hasOwn(value, 'create'))) {
93
+ throw new Error('Prisma 8 fixture relations require connect or create');
94
+ }
95
+ const target = namespaces[relation.to.namespace].models[relation.to.model];
96
+ const operation = Object.hasOwn(value, 'connect') ? 'connect' : 'create';
97
+ const map = (item) => {
98
+ if (!(0, fixture_document_1.isFixtureRecord)(item))
99
+ throw new Error('Invalid fixture relation data');
100
+ return relationData(item, target);
101
+ };
102
+ const input = value[operation];
103
+ const mapped = Array.isArray(input) ? input.map(map) : map(input);
104
+ return [
105
+ field,
106
+ (mutator) => mutator[operation](mapped),
107
+ ];
108
+ }));
109
+ }
110
+ function adapt(orm, runtime, check = () => { }) {
111
+ const delegates = Object.create(null);
112
+ const byModel = new Map();
113
+ for (const [alias, model] of aliases) {
114
+ if (!model)
115
+ continue;
116
+ const { namespace, name } = model;
117
+ const qualified = `${namespace}.${name}`;
118
+ let delegate = byModel.get(qualified);
119
+ if (!delegate) {
120
+ const metadata = namespaces[namespace].models[name];
121
+ delegate = {
122
+ async create({ data }) {
123
+ check();
124
+ return orm[namespace][name].create(relationData(data, metadata));
125
+ },
126
+ async update({ where, data }) {
127
+ check();
128
+ return orm[namespace][name]
129
+ .where(relationData(where, metadata))
130
+ .update(relationData(data, metadata));
131
+ },
132
+ };
133
+ byModel.set(qualified, delegate);
134
+ }
135
+ delegates[alias] = delegate;
136
+ }
137
+ const adapted = Object.assign(delegates, {
138
+ async $executeRawUnsafe(sql) {
139
+ check();
140
+ return runtime.execute(plan(sql));
141
+ },
142
+ });
143
+ exports.prisma8PreservedTables.set(adapted, preserveTables);
144
+ return adapted;
145
+ }
146
+ return Object.assign(adapt(db.orm, db.runtime()), {
147
+ $disconnect: () => db.close(),
148
+ async $transaction(action, { timeout = 60_000 } = {}) {
149
+ if (!Number.isSafeInteger(timeout) ||
150
+ timeout <= 0 ||
151
+ timeout > 2_147_483_647)
152
+ throw new Error('Invalid transaction timeout');
153
+ return db.transaction(async (tx) => {
154
+ // PostgreSQL rejects this setting before any fixtures on versions older than 17.
155
+ // The database deadline also cancels in-flight queries and rolls back idle callbacks.
156
+ await tx.execute(plan(`SET LOCAL transaction_timeout = '${timeout}ms'`));
157
+ const deadline = Date.now() + timeout;
158
+ let closed = false;
159
+ let timer;
160
+ const check = () => {
161
+ if (closed || Date.now() >= deadline)
162
+ throw new Error('Fixture transaction closed or timed out');
163
+ };
164
+ try {
165
+ const result = await Promise.race([
166
+ Promise.resolve().then(() => action(adapt(tx.orm, tx, check))),
167
+ new Promise((_, reject) => {
168
+ timer = setTimeout(() => {
169
+ closed = true;
170
+ reject(new Error('Fixture transaction timed out'));
171
+ }, timeout);
172
+ }),
173
+ ]);
174
+ check();
175
+ return result;
176
+ }
177
+ finally {
178
+ closed = true;
179
+ clearTimeout(timer);
180
+ }
181
+ });
182
+ },
183
+ });
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skmdev/prisma-fixtures",
3
- "version": "0.1.1",
3
+ "version": "1.0.0-rc.1",
4
4
  "description": "YAML and JSON fixtures with references, templates and processors for Prisma.",
5
5
  "license": "MIT",
6
6
  "author": "skmdev <skmdev29@gmail.com>",
@@ -66,10 +66,12 @@
66
66
  "devDependencies": {
67
67
  "@prisma/adapter-pg": "7.10.0",
68
68
  "@prisma/client": "7.10.0",
69
+ "@prisma/orm-postgres": "8.0.0-rc.12",
70
+ "@prisma/prisma7": "7.10.0",
69
71
  "@types/ejs": "3.1.5",
70
72
  "@types/node": "22.19.15",
71
73
  "prettier": "3.8.3",
72
- "prisma": "7.10.0",
74
+ "prisma": "8.0.0-rc.17",
73
75
  "typescript": "5.9.3"
74
76
  },
75
77
  "overrides": {
@@ -58,9 +58,9 @@
58
58
  },
59
59
  "definitions": {
60
60
  "entityName": {
61
- "description": "Prisma model/delegate name.",
61
+ "description": "Prisma model/delegate name, optionally namespace-qualified for v8.",
62
62
  "type": "string",
63
- "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,127}$"
63
+ "pattern": "^(?:[A-Za-z][A-Za-z0-9_-]{0,127}\\.)?[A-Za-z][A-Za-z0-9_-]{0,127}$"
64
64
  },
65
65
  "fieldName": {
66
66
  "description": "Prisma relation field name.",