@prisma/orm-mongo 8.0.0-rc.9-dev.7 → 8.0.0-rc.9-dev.9
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/package.json +8 -8
- package/skills/prisma-8/SKILL.md +16 -13
- package/skills/prisma-8/references/contract.md +60 -31
- package/skills/prisma-8/references/debug.md +44 -41
- package/skills/prisma-8/references/migration-model.md +2 -2
- package/skills/prisma-8/references/migration-review.md +28 -15
- package/skills/prisma-8/references/migrations.md +85 -69
- package/skills/prisma-8/references/queries-mongo.md +16 -16
- package/skills/prisma-8/references/queries-postgres.md +78 -78
- package/skills/prisma-8/references/queries.md +54 -28
- package/skills/prisma-8/references/quickstart.md +32 -41
- package/skills/prisma-8/references/runtime.md +76 -54
- package/skills/prisma-8/references/supabase.md +15 -28
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.9-to-8.0.0-rc.10/instructions.md +1 -0
|
@@ -34,7 +34,7 @@ This skill does **not** cover migrating from another ORM (Drizzle, Prisma 6/7, S
|
|
|
34
34
|
- **Authoring mode**: how you write the contract. `psl` (Prisma Schema Language, default) or `typescript` (programmatic builder, optionally paired with the Vite plugin for auto-emit during `vite dev` — see `references/build.md`).
|
|
35
35
|
- **Façade packages.** The scaffold installs exactly one façade per target — `@internal/postgres` (or `@internal/mongo`). User code imports from façade subpaths (`@internal/postgres/config`, `@internal/postgres/runtime`, `@internal/postgres/contract-builder`). The façade bakes in the family / target / adapter / driver wiring; never reach past it. See `references/contract.md` for the full list.
|
|
36
36
|
- **`db.ts`**: the runtime entry point. Lives next to the contract source at `src/prisma/db.ts`. Imports the contract artefacts and exports a `db` value the rest of the app uses.
|
|
37
|
-
- **Marker**: a `
|
|
37
|
+
- **Marker**: a row in the `prisma_contract.marker` table (Postgres) or a document in the `_prisma_migrations` collection (Mongo) that records the contract hash per contract space. Lets PN detect drift between contract and live DB. Created by `db init` (greenfield / first-touch orientation) or `db sign` (brownfield).
|
|
38
38
|
|
|
39
39
|
### Canonical on-disk layout
|
|
40
40
|
|
|
@@ -70,7 +70,7 @@ Three things to internalise:
|
|
|
70
70
|
|
|
71
71
|
**Contributors building extension packages or aggregate-root monorepo packages use a different layout** — `src/contract.{prisma,ts}` (no `prisma/` subdir) + `migrations/<timestamp>_<slug>/` (no `app/` segment). That distinction is intentional; see `references/contract.md` for which path applies to you.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
`prisma orm init` scaffolds this layout by default: the contract at `src/prisma/contract.prisma` (or `.ts`) and `db.ts` beside it. A project scaffolded by an older init may have a top-level `prisma/` directory instead — read the `contract` path in `prisma.config.ts` rather than assuming either.
|
|
74
74
|
|
|
75
75
|
## Your first arc — connect, write, read
|
|
76
76
|
|
|
@@ -85,18 +85,20 @@ import { db } from './prisma/db';
|
|
|
85
85
|
|
|
86
86
|
// Write a row against the starter model. Adapt the field names to whatever
|
|
87
87
|
// model your contract source actually declares — read it first.
|
|
88
|
-
await db.orm.User.create({ email: 'alice@example.com' });
|
|
88
|
+
await db.orm.public.User.create({ email: 'alice@example.com' });
|
|
89
89
|
|
|
90
90
|
// Read it back.
|
|
91
|
-
const users = await db.orm.User.select('id', 'email').all();
|
|
91
|
+
const users = await db.orm.public.User.select('id', 'email').all();
|
|
92
92
|
console.log(users);
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
If that prints `[{ id: 1, email: 'alice@example.com' }]`, the project is wired end-to-end and the user has crossed from *"I have a project"* to *"I'm building."*
|
|
96
96
|
|
|
97
|
-
`db.orm.<Model>` is the default ORM lane — model-shaped, fully typed against the contract, lazily connects to the database on first use (it picks up `DATABASE_URL` from `.env` via the runtime's `dotenv/config`-loaded environment). The deeper `references/queries.md` reference covers the rest of the supported surface (filters, joins, transactions, the SQL builder) when the user is ready — and names the gaps (
|
|
97
|
+
`db.orm.<ns>.<Model>` is the default ORM lane — model-shaped, fully typed against the contract, lazily connects to the database on first use (it picks up `DATABASE_URL` from `.env` via the runtime's `dotenv/config`-loaded environment). The deeper `references/queries.md` reference covers the rest of the supported surface (filters, joins, transactions, the SQL builder, raw SQL via `db.raw.sql`, prepared statements) when the user is ready — and names the gaps (TypedSQL is not available).
|
|
98
98
|
|
|
99
|
-
> **
|
|
99
|
+
> **SQLite target:** `prisma orm init` scaffolds only `postgres` and `mongodb`; a SQLite project is wired by hand with the `@internal/sqlite` façade (`references/runtime.md` § *Switch between Postgres, SQLite, and Mongo*; `examples/prisma-8-demo-sqlite`). SQLite has no schemas, so that façade exposes the unbound namespace directly — write `db.orm.User` rather than `db.orm.public.User`.
|
|
100
|
+
>
|
|
101
|
+
> **Mongo target:** the snippet above is SQL-target shape. On `@internal/mongo`, `db.orm` is keyed by the collection's storage name (`@@map(...)`, or the lowercased model name if no `@@map`), so the same arc reads `await db.orm.users.create(...)` / `await db.orm.users.select('id', 'email').all()` — not `db.orm.public.User`. Full rule and rewrite recipe in `references/queries.md` § *MongoDB ORM addressing*.
|
|
100
102
|
|
|
101
103
|
**Prerequisites for the arc to work.** All three paths leave these in place by the time you reach the arc:
|
|
102
104
|
|
|
@@ -127,10 +129,10 @@ The first **arc** — once oriented — is **connect → write → read**. Not e
|
|
|
127
129
|
Before saying anything specific to the user, read:
|
|
128
130
|
|
|
129
131
|
- `prisma.config.ts` at the repo root — what target (`postgres` / `mongodb`) is wired, what `contract:` path it declares, what extensions are installed.
|
|
130
|
-
- The contract source the config declares (canonically `src/prisma/contract.prisma` or `src/prisma/contract.ts`; a project
|
|
132
|
+
- The contract source the config declares (canonically `src/prisma/contract.prisma` or `src/prisma/contract.ts`; a project scaffolded by an older init may have it at `prisma/contract.{prisma,ts}` instead — check the `contract` field of the config) — what starter models, if any, exist.
|
|
131
133
|
- `src/prisma/db.ts` (next to the contract) — the runtime entry point.
|
|
132
134
|
- `.env` / `.env.example` — is `DATABASE_URL` set, or only the example?
|
|
133
|
-
- Optionally `pnpm prisma
|
|
135
|
+
- Optionally `pnpm prisma db verify` — does the live DB match the contract? (Exit `4` with findings means drift or no marker; `2` means it could not run.)
|
|
134
136
|
|
|
135
137
|
Then **say the contract path back to the user, with its role attached**. Something like: *"Your contract is at `src/prisma/contract.prisma`, and it currently declares a `User` model. The contract describes your app — every query type, migration, and runtime type the framework gives you flows from this file. Let's get your app connected to a database next."* The exact wording is up to the agent; what matters is that the user leaves the first response knowing *where the contract is* and *that it is the source of truth*.
|
|
136
138
|
|
|
@@ -139,9 +141,9 @@ Then **say the contract path back to the user, with its role attached**. Somethi
|
|
|
139
141
|
The motivation is *"so your app can actually run against your database"*, not *"so the prerequisite checklist passes"*. The mechanics depend on what's already in place from Step 1:
|
|
140
142
|
|
|
141
143
|
- **Everything already wired.** Go straight to writing and reading a row (see *Your first arc — connect, write, read* above). Adapt the snippet to whatever model the contract declares.
|
|
142
|
-
- **`DATABASE_URL` not set.** Have the user set it in `.env` (not in `prisma.config.ts` — see Pitfall 5). Then `pnpm prisma
|
|
143
|
-
- **Database is connectable but not yet aware of the contract** (marker row missing; `db verify` reports drift). Run `pnpm prisma
|
|
144
|
-
- **Contract is empty** (bootstrap left the source blank). Add **one** model with **two** fields (e.g. `User { id, email }`), `pnpm prisma
|
|
144
|
+
- **`DATABASE_URL` not set.** Have the user set it in `.env` (not in `prisma.config.ts` — see Pitfall 5). Then `pnpm prisma db init` to apply the current contract to that database and write the marker row. Now the app can connect.
|
|
145
|
+
- **Database is connectable but not yet aware of the contract** (marker row missing; `db verify` reports drift). Run `pnpm prisma db init`. (`db update` is the alternative for quick dev cycles — it's looser, doesn't write a migration history, and is what users reach for when they want to iterate on the schema fast. Mention it if the user asks how to make schema changes flow to the DB; don't pre-explain it.)
|
|
146
|
+
- **Contract is empty** (bootstrap left the source blank). Add **one** model with **two** fields (e.g. `User { id, email }`), `pnpm prisma contract emit`, then `pnpm prisma db init`. Minimal — get the round-trip working, *then* extend.
|
|
145
147
|
|
|
146
148
|
The user encounters `db init` (and optionally `db update`, `contract emit`) here because they're the commands their current move *requires*. They learn what those commands are by using them.
|
|
147
149
|
|
|
@@ -186,40 +188,30 @@ The flags `init` accepts (run `prisma orm init --help` for the source of truth):
|
|
|
186
188
|
|
|
187
189
|
- `--target <db>` — `postgres` or `mongodb`.
|
|
188
190
|
- `--authoring <style>` — `psl` or `typescript`.
|
|
189
|
-
- `--schema-path <path>` —
|
|
190
|
-
- `--confirm <directory name>` — grant the reinit consent non-interactively. Re-running init in a scaffolded directory asks you to type the directory name back before it overwrites; non-interactive runs pass the name with this flag instead.
|
|
191
|
+
- `--schema-path <path>` — where to write the starter schema. Defaults to `src/prisma/contract.prisma` (or `src/prisma/contract.ts` with `--authoring typescript`), the canonical layout above. The extension must agree with `--authoring` (`CLI.INIT_AUTHORING_SCHEMA_PATH_MISMATCH` otherwise).
|
|
192
|
+
- `--confirm <directory name>` — grant the reinit consent non-interactively. Re-running init in a scaffolded directory asks you to type the directory name back before it overwrites; non-interactive runs pass the name with this flag instead (`--yes` does not grant it).
|
|
191
193
|
- `--write-env` — also write `.env` (default writes only `.env.example`; `.env` stays under your control).
|
|
192
194
|
- `--probe-db` — connect to `DATABASE_URL` once and check the server version against the target's minimum.
|
|
193
|
-
- `--strict-probe` — fail init if the probe fails (
|
|
195
|
+
- `--strict-probe` — fail init if the probe fails (errors without `--probe-db`).
|
|
194
196
|
- `--skip-install` — skip dependency install + initial contract emit.
|
|
195
|
-
- `--
|
|
197
|
+
- `--keep-previous-facade` — when re-running init to switch targets, keep the previous target package in `package.json`.
|
|
198
|
+
|
|
199
|
+
`init` does not install agent skills and has no `--skip-skills` flag: the `prisma-8` skill ships inside the `@prisma/orm-*` package the project installs, and the family-level `prisma init` / `prisma skills sync` commands copy it into the agent harness directories.
|
|
196
200
|
|
|
197
201
|
`init` writes (when it runs cleanly):
|
|
198
202
|
|
|
199
|
-
- `prisma.config.ts` at the project root.
|
|
200
|
-
- The contract source at `--schema-path`
|
|
203
|
+
- `prisma.config.ts` at the project root (envelope form — see `references/contract.md`).
|
|
204
|
+
- The contract source at `--schema-path` (`src/prisma/contract.prisma` by default).
|
|
201
205
|
- `db.ts` in the same directory as the contract source.
|
|
202
206
|
- `prisma-next.md` — a human quick-reference.
|
|
203
207
|
- `.env.example` (and `.env` if `--write-env`).
|
|
204
|
-
- Updates `package.json` (deps +
|
|
205
|
-
- Installs deps and runs `prisma
|
|
206
|
-
- Registers Prisma 8 skills with the local agent runtime.
|
|
207
|
-
|
|
208
|
-
**If you took `init`'s default and ended up with a top-level `prisma/` directory** (TML-2532), the cleanup is one move + one config edit:
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
mkdir -p src && mv prisma src/prisma
|
|
212
|
-
# Then update prisma.config.ts so `contract` reads
|
|
213
|
-
# 'src/prisma/contract.prisma' (or .ts) instead of 'prisma/contract.prisma'.
|
|
214
|
-
pnpm prisma-cli contract emit # re-emits contract.json + contract.d.ts under src/prisma/
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Do this before running `db init` — once the marker row is written, restructuring is harder.
|
|
208
|
+
- Updates `package.json` (deps + a `contract:emit` script) and `tsconfig.json` (required compiler options).
|
|
209
|
+
- Installs deps and runs `prisma contract emit` once. If the install or the emit fails, the scaffold is still on disk and init exits `4` (`CLI.INIT_INSTALL_FAILED`) or `5` (`CLI.INIT_EMIT_FAILED`) with the step to re-run.
|
|
218
210
|
|
|
219
211
|
After init succeeds, the path converges on *Your first arc — connect, write, read* above. `init` has already seeded a starter contract with `User` and `Post` models (with a relation between them) and run `contract emit` once; the only remaining prerequisites are setting `DATABASE_URL` and initialising the database. Two commands:
|
|
220
212
|
|
|
221
213
|
1. Set `DATABASE_URL` in `.env` (copy from `.env.example`).
|
|
222
|
-
2. Initialise the database: `pnpm prisma
|
|
214
|
+
2. Initialise the database: `pnpm prisma db init`. Creates tables, indexes, constraints, and writes the marker row — using the starter contract `init` generated.
|
|
223
215
|
|
|
224
216
|
Then run the snippet from *Your first arc* above against the `User` model. When the user is ready to extend the contract — add more models, change fields, add relations — chain to `references/contract.md`. For more queries, chain to `references/queries.md`.
|
|
225
217
|
|
|
@@ -232,15 +224,14 @@ The concept: against an existing database with no PN contract, `contract infer`
|
|
|
232
224
|
```bash
|
|
233
225
|
mkdir my-app && cd my-app
|
|
234
226
|
pnpm init
|
|
235
|
-
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
|
|
236
|
-
|
|
237
|
-
# scaffold lands; you'll overwrite the starter schema below
|
|
227
|
+
pnpm dlx prisma@latest orm init --yes --target postgres --authoring psl
|
|
228
|
+
# scaffold lands at src/prisma/; you'll overwrite the starter schema below
|
|
238
229
|
```
|
|
239
230
|
|
|
240
231
|
Then, with `DATABASE_URL` set in `.env`:
|
|
241
232
|
|
|
242
233
|
```bash
|
|
243
|
-
pnpm prisma
|
|
234
|
+
pnpm prisma contract infer --db "$DATABASE_URL" --output src/prisma/contract.prisma
|
|
244
235
|
```
|
|
245
236
|
|
|
246
237
|
(Note: the flag is `--output`, not `--out`. Run `prisma contract infer --help` for the full surface.)
|
|
@@ -255,9 +246,9 @@ The agent should pause here and read the inferred PSL. Symptoms a re-author pass
|
|
|
255
246
|
Then re-emit and sign:
|
|
256
247
|
|
|
257
248
|
```bash
|
|
258
|
-
pnpm prisma
|
|
259
|
-
pnpm prisma
|
|
260
|
-
pnpm prisma
|
|
249
|
+
pnpm prisma contract emit
|
|
250
|
+
pnpm prisma db sign
|
|
251
|
+
pnpm prisma db verify # exit 0 immediately after a pull; exit 4 with findings if the DB drifts later
|
|
261
252
|
```
|
|
262
253
|
|
|
263
254
|
Then run the snippet from *Your first arc — connect, write, read* above, using one of your existing tables in place of the starter model. The arc is the same; only the path that got you there differs.
|
|
@@ -292,7 +283,7 @@ Switch authoring later by re-running `prisma orm init` in the same directory. Th
|
|
|
292
283
|
2. **`init` doesn't connect to your database.** It only scaffolds files and installs dependencies (and runs the initial `contract emit`). You connect with `db init` / `db update` / `db migrate`. If `init` succeeds and queries fail, the issue is `DATABASE_URL`, not `init`.
|
|
293
284
|
3. **Treating inferred PSL as the final contract.** `contract infer` produces a starting point. Don't `db sign` against a contract you haven't read.
|
|
294
285
|
4. **Forgetting to emit after editing the contract.** The contract artefacts (`contract.json`, `contract.d.ts`) are stale until you run `contract emit`. If the type-checker says a model "doesn't exist", you skipped emit.
|
|
295
|
-
5. **Setting `DATABASE_URL` in `prisma.config.ts` instead of `.env`.**
|
|
286
|
+
5. **Setting `DATABASE_URL` in `prisma.config.ts` instead of `.env`.** Nothing reads `.env` on its own: the scaffolded `prisma.config.ts` starts with `import 'dotenv/config'`, and that import is what loads `.env` into `process.env` before the config (and the CLI running it) reads `process.env['DATABASE_URL']`. Keep the import; a config without it sees no `.env` values. Hardcoding the URL leaks credentials and bypasses per-environment overrides. See `references/runtime.md`.
|
|
296
287
|
6. **Hand-editing `contract.json` or `contract.d.ts`.** They're emitted artefacts; the next `contract emit` overwrites your changes. Edit the source instead.
|
|
297
288
|
7. **Using `--out` for `contract infer`.** The flag is `--output`.
|
|
298
289
|
|
|
@@ -317,7 +308,7 @@ This skill is intentionally body-only; `prisma orm init --help`, `contract infer
|
|
|
317
308
|
- [ ] Confirmed the user's target (`postgres` / `mongodb`) and authoring mode (`psl` / `typescript`).
|
|
318
309
|
- [ ] **First-touch orientation:** read `prisma.config.ts`, the contract source, `db.ts`, and `.env` before proposing anything — didn't assume what the scaffold tool / teammate left in place.
|
|
319
310
|
- [ ] **Greenfield path:** ran `prisma orm init` from the project directory — no positional project-name argument.
|
|
320
|
-
- [ ] **All paths:** the project ended up in the canonical `src/prisma/contract.{prisma,ts}` + `src/prisma/db.ts` + `migrations/app/` layout
|
|
311
|
+
- [ ] **All paths (application projects):** the project ended up in the canonical `src/prisma/contract.{prisma,ts}` + `src/prisma/db.ts` + `migrations/app/` layout (what `init` scaffolds by default). An extension or aggregate-root package keeps its own `src/contract.{prisma,ts}` + `migrations/<timestamp>_<slug>/` layout — do not relocate it.
|
|
321
312
|
- [ ] **Brownfield path:** ran `contract infer --db "$DATABASE_URL" --output src/prisma/contract.prisma`, reviewed the result, then `contract emit` + `db sign`.
|
|
322
313
|
- [ ] Set `DATABASE_URL` in `.env` and confirmed the value is reachable.
|
|
323
314
|
- [ ] Initialised the DB (`db init` greenfield / first-touch orientation) or signed the marker (`db sign` brownfield).
|
|
@@ -8,12 +8,12 @@ This skill covers the **runtime entry point** — `db.ts` — and how to compose
|
|
|
8
8
|
## When to Use
|
|
9
9
|
|
|
10
10
|
- User is wiring up `db.ts` for the first time (post-init).
|
|
11
|
-
- User wants to add middleware (
|
|
11
|
+
- User wants to add middleware (lints, budgets, cache, custom).
|
|
12
12
|
- User wants per-environment config (dev vs prod, multi-region).
|
|
13
13
|
- User wants to switch between the Postgres, SQLite, and Mongo façades.
|
|
14
14
|
- User wants to wrap operations in `db.transaction(...)` (Postgres and SQLite).
|
|
15
15
|
- User is running a one-off script (`tsx my-script.ts`, Node CLI, CI task) and the process won't exit after queries finish, or they need script teardown (`db.close()`, `await using`).
|
|
16
|
-
- User mentions: *db.ts, postgres(), mongo(), middleware,
|
|
16
|
+
- User mentions: *db.ts, postgres(), mongo(), middleware, lints, budgets, cache, query log, slow query, DATABASE_URL, .env, connection pool, poolOptions, dev vs prod, transactions, read replicas, multi-database, script won't exit, hangs, db.close, db.end, close connection, pool.end, await using*.
|
|
17
17
|
|
|
18
18
|
## When Not to Use
|
|
19
19
|
|
|
@@ -29,8 +29,8 @@ This skill covers the **runtime entry point** — `db.ts` — and how to compose
|
|
|
29
29
|
- **`db.ts` is the runtime entry point.** Imports the runtime factory from the `@internal/<target>` façade (`@internal/postgres/runtime`, `@internal/sqlite/runtime`, or `@internal/mongo/runtime`), the contract artefacts (`contract.json` + the `Contract` type from `contract.d.ts`), and any middleware. Exports a `db` value the rest of your app imports.
|
|
30
30
|
- **The façade's runtime factory is the only surface user-authored `db.ts` imports from.** Each factory is a *default* export. For Postgres: `import postgres from '@internal/postgres/runtime'`; SQLite: `import sqlite from '@internal/sqlite/runtime'`; Mongo: `import mongo from '@internal/mongo/runtime'`. The factory signature is `<Target><Contract>(options)` — a single type parameter (the `Contract` type from `contract.d.ts`), and one options object.
|
|
31
31
|
- **Lazy connect.** The factory does not connect to the database synchronously. Static query surfaces (`db.sql`, `db.orm`) are available immediately; the driver / pool is instantiated on the first call that needs a runtime (or when you explicitly call `await db.connect({ url })`). This is why `db.ts` can be imported in modules that load before the env is ready.
|
|
32
|
-
- **Middleware composes in order.** The first middleware in the `middleware: [...]` array runs *outermost* —
|
|
33
|
-
- **`prisma.config.ts` vs `.env`.** The config (`
|
|
32
|
+
- **Middleware composes in order.** The first middleware in the `middleware: [...]` array runs *outermost* — its `beforeQuery` sees the operation first, and `interceptQuery` hooks are consulted in registration order with the first non-`undefined` result winning. Register a cache first so it gets first claim on a hit. Every middleware's `afterQuery` still fires on a cache hit, with `result.source: 'middleware'`.
|
|
33
|
+
- **`prisma.config.ts` vs `.env`.** The config (`definePrismaConfig({ orm: ormConfig({ contract, db, extensions, migrations }) })` — see `references/contract.md`) is for static project shape: contract path, installed extensions, migrations directory, default connection string. `.env` is for per-environment values (`DATABASE_URL`, secrets). Nothing reads `.env` on its own: the scaffolded `prisma.config.ts` starts with `import 'dotenv/config'`, and that import is what loads `.env` into `process.env` before the config (and the CLI running it) reads `process.env['DATABASE_URL']`. Keep the import; a config without it sees no `.env` values. Hardcoding `DATABASE_URL` in the config file leaks credentials and bypasses per-env overrides.
|
|
34
34
|
- **Build-system / dev-server integration is a separate skill.** `vite dev` auto-emit lives in `references/build.md`. The runtime side (this skill) reads `contract.json` / `contract.d.ts` regardless of how they got onto disk, so the two skills compose cleanly.
|
|
35
35
|
|
|
36
36
|
## Workflow — Basic `db.ts`
|
|
@@ -47,11 +47,11 @@ import contractJson from './contract.json' with { type: 'json' };
|
|
|
47
47
|
|
|
48
48
|
export const db = postgres<Contract>({
|
|
49
49
|
contractJson,
|
|
50
|
-
url: process.env['DATABASE_URL']
|
|
50
|
+
url: process.env['DATABASE_URL']!,
|
|
51
51
|
});
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
(
|
|
54
|
+
(The rest of `src/` imports from `./prisma/db` or `../prisma/db` depending on depth.)
|
|
55
55
|
|
|
56
56
|
Three things to know:
|
|
57
57
|
|
|
@@ -59,7 +59,7 @@ Three things to know:
|
|
|
59
59
|
- **`with { type: 'json' }` is required.** Node's ESM JSON-import-attribute spec. Without it, the import errors.
|
|
60
60
|
- **`url` is optional at construct time.** If `DATABASE_URL` is not set when `db.ts` loads, the factory still returns a client; you can call `await db.connect({ url })` later. The factory throws lazily — only when a runtime is actually needed.
|
|
61
61
|
|
|
62
|
-
The Mongo façade has the same construction shape — `import mongo from '@internal/mongo/runtime'` — and the same `db.connect(...)` / `db.close()` lifecycle methods. **The Mongo façade does not expose `db.transaction(...)`.** See *What Prisma 8 doesn't do yet* for the workaround. **The ORM surface differs in one place: keys.** On Mongo, `db.orm` is keyed by the collection's storage name (from `@@map(...)`, or the lowercased model name if no `@@map` is set), not by the PSL model name — so `model User { … @@map("users") }` is reached at `db.orm.users`, not `db.orm.User`. The SQL builder lane (`db.sql.<table>`) doesn't exist on Mongo at all (`db.sql` is `undefined`). See `references/queries.md` § *MongoDB ORM addressing* for the full rule and a rewrite recipe for SQL-target examples.
|
|
62
|
+
The Mongo façade has the same construction shape — `import mongo from '@internal/mongo/runtime'` — and the same `db.connect(...)` / `db.close()` lifecycle methods. **The Mongo façade does not expose `db.transaction(...)`.** See *What Prisma 8 doesn't do yet* for the workaround. **The ORM surface differs in one place: keys.** On Mongo, `db.orm` is keyed by the collection's storage name (from `@@map(...)`, or the lowercased model name if no `@@map` is set), not by the PSL model name — so `model User { … @@map("users") }` is reached at `db.orm.users`, not `db.orm.public.User`. The SQL builder lane (`db.sql.<ns>.<table>`) doesn't exist on Mongo at all (`db.sql` is `undefined`). See `references/queries.md` § *MongoDB ORM addressing* for the full rule and a rewrite recipe for SQL-target examples.
|
|
63
63
|
|
|
64
64
|
## Workflow — Running as a script (teardown)
|
|
65
65
|
|
|
@@ -71,8 +71,8 @@ The concept: short scripts that connect, query, then expect the process to exit
|
|
|
71
71
|
// src/scripts/hello.ts
|
|
72
72
|
import { db } from '../prisma/db';
|
|
73
73
|
|
|
74
|
-
const created = await db.orm.User.create({ email: 'alice@example.com', name: 'Alice' });
|
|
75
|
-
const read = await db.orm.User.first();
|
|
74
|
+
const created = await db.orm.public.User.create({ email: 'alice@example.com', name: 'Alice' });
|
|
75
|
+
const read = await db.orm.public.User.first();
|
|
76
76
|
console.log({ created, read });
|
|
77
77
|
|
|
78
78
|
await db.close();
|
|
@@ -88,7 +88,7 @@ import contractJson from '../prisma/contract.json' with { type: 'json' };
|
|
|
88
88
|
|
|
89
89
|
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
|
|
90
90
|
|
|
91
|
-
const user = await db.orm.User.first();
|
|
91
|
+
const user = await db.orm.public.User.first();
|
|
92
92
|
console.log(user);
|
|
93
93
|
// db.close() runs automatically when the script module exits.
|
|
94
94
|
```
|
|
@@ -101,7 +101,7 @@ This is the most important rule in this section. `await using db = postgres(...)
|
|
|
101
101
|
// DO NOT do this — closes the pool after every request.
|
|
102
102
|
app.get('/users', async (req, res) => {
|
|
103
103
|
await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
|
|
104
|
-
const users = await db.orm.User.all();
|
|
104
|
+
const users = await db.orm.public.User.all();
|
|
105
105
|
res.json(users);
|
|
106
106
|
});
|
|
107
107
|
```
|
|
@@ -116,7 +116,7 @@ export const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_U
|
|
|
116
116
|
import { db } from '../prisma/db';
|
|
117
117
|
|
|
118
118
|
app.get('/users', async (req, res) => {
|
|
119
|
-
const users = await db.orm.User.all();
|
|
119
|
+
const users = await db.orm.public.User.all();
|
|
120
120
|
res.json(users);
|
|
121
121
|
});
|
|
122
122
|
```
|
|
@@ -127,45 +127,46 @@ Servers (HTTP handlers, workers in a request loop) **do not call `db.close()`**
|
|
|
127
127
|
|
|
128
128
|
- **`close()` is idempotent.** Calling it twice is a no-op.
|
|
129
129
|
- **`close()` is terminal.** There is no reconnect on a closed `db` — construct a new client if you need another connection. After close, `db.runtime()`, `db.connect(...)`, `db.transaction(...)`, and `db.prepare(...)` reject with `Error('<target> client is closed')` (e.g. `'Postgres client is closed'`, `'SQLite client is closed'`, `'Mongo client is closed'`).
|
|
130
|
-
- **`close()` does not abort in-flight queries.** `await` outstanding work before calling `close()`. Async iterators from `db.runtime().
|
|
130
|
+
- **`close()` does not abort in-flight queries.** `await` outstanding work before calling `close()`. Async iterators from `db.runtime().query(plan)` and `PreparedStatement` handles held after `close()` fail on their next call.
|
|
131
131
|
- **Ownership.** `close()` releases only what the façade constructed (`pg.Pool` from `{ url }`, `MongoClient` from `{ url }` / `{ uri, dbName }`, SQLite handle from `{ path }`). If you supplied your own `pg.Pool` / `pg.Client` (Postgres `pg:` option), `mongodb.MongoClient` (Mongo `mongoClient:` option), or a pre-built `binding`, `db.close()` does **not** touch those — you own their lifecycle.
|
|
132
132
|
|
|
133
133
|
**`db.end()` does not exist.** The universal `node-postgres` name is `pool.end()` on a `pg.Pool`; the Prisma 8 runtime client is not a `pg.Pool`. The right call is `await db.close()`.
|
|
134
134
|
|
|
135
|
-
## Workflow —
|
|
135
|
+
## Workflow — Custom middleware (query log, slow-query warning)
|
|
136
136
|
|
|
137
|
-
The concept:
|
|
137
|
+
The concept: a middleware is a plain object with `name`, `familyId: 'sql'`, and any of the hooks `beforeQuery`, `interceptQuery`, `afterQuery`. There is no separate telemetry package — observe queries with `afterQuery`, which fires once per execution after the rows are consumed, with `result.latencyMs`, `result.rowCount`, and `result.source` (`'driver'` or `'middleware'` for a cache hit). The `SqlMiddleware` type comes from `@prisma/orm-postgres/family-runtime`. `examples/prisma-8-demo/src/prisma/slow-query-warning.ts` is the canonical example:
|
|
138
138
|
|
|
139
139
|
```typescript
|
|
140
|
-
import
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
140
|
+
import type { SqlMiddleware } from '@prisma/orm-postgres/family-runtime';
|
|
141
|
+
|
|
142
|
+
export function slowQueryWarning(options?: { readonly thresholdMs?: number }): SqlMiddleware {
|
|
143
|
+
const thresholdMs = options?.thresholdMs ?? 250;
|
|
144
|
+
return {
|
|
145
|
+
name: 'slow-query-warning',
|
|
146
|
+
familyId: 'sql',
|
|
147
|
+
async afterQuery(plan, result, ctx) {
|
|
148
|
+
if (result.latencyMs <= thresholdMs) return;
|
|
149
|
+
ctx.log.warn({
|
|
150
|
+
code: 'APP.SLOW_QUERY',
|
|
151
|
+
message: `Query took ${result.latencyMs}ms (threshold: ${thresholdMs}ms)`,
|
|
152
|
+
details: { sql: plan.sql, rowCount: result.rowCount, source: result.source },
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
156
157
|
```
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
A "log every query" middleware is the same shape without the threshold. The runtime also exposes its last telemetry event through `db.runtime().telemetry()`.
|
|
159
160
|
|
|
160
161
|
## Workflow — Lints and budgets middleware
|
|
161
162
|
|
|
162
163
|
The concept: lints catch authoring mistakes that survive type-check (e.g. `DELETE` without a `WHERE`, `SELECT` without a `LIMIT` on a large table); budgets enforce row-count and latency ceilings at runtime. Both surface findings through the structured-error envelope so an agent can branch on the code.
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
Both are exported from the façade's `family-runtime` subpath — `@prisma/orm-postgres/family-runtime` (and `@prisma/orm-sqlite/family-runtime`, `@prisma/orm-mongo/family-runtime`). `examples/prisma-8-demo/src/prisma/db.ts` shows the canonical import.
|
|
165
166
|
|
|
166
167
|
```typescript
|
|
167
168
|
import postgres from '@internal/postgres/runtime';
|
|
168
|
-
import { budgets, lints } from '@
|
|
169
|
+
import { budgets, lints } from '@prisma/orm-postgres/family-runtime';
|
|
169
170
|
import type { Contract } from './contract.d';
|
|
170
171
|
import contractJson from './contract.json' with { type: 'json' };
|
|
171
172
|
|
|
@@ -193,19 +194,40 @@ export const db = postgres<Contract>({
|
|
|
193
194
|
});
|
|
194
195
|
```
|
|
195
196
|
|
|
196
|
-
For the full option surface, read the source: `packages/2-sql/5-runtime/src/middleware/lints.ts` and `.../budgets.ts`. The `severities` keys (`selectStar`, `noLimit`, `deleteWithoutWhere`, `updateWithoutWhere`, `readOnlyMutation` for lints; `rowCount`, `latency` for budgets) are the source of truth; do not extrapolate to a key that ripgrep can't find.
|
|
197
|
+
For the full option surface, read the source: `packages/2-sql/5-runtime/src/middleware/lints.ts` and `.../budgets.ts`. The `severities` keys (`selectStar`, `noLimit`, `deleteWithoutWhere`, `updateWithoutWhere`, `readOnlyMutation` for lints; `rowCount`, `latency` for budgets) are the source of truth; do not extrapolate to a key that ripgrep can't find. `lints()` with no argument uses the default severities.
|
|
198
|
+
|
|
199
|
+
## Workflow — Cache middleware
|
|
200
|
+
|
|
201
|
+
The concept: `@prisma/orm-extension-middleware-cache` ships an opt-in read cache built on the `interceptQuery` hook. On a hit the driver is never called; on a miss the rows are buffered and committed to the store when the query completes. Caching is strictly opt-in per query: only a plan annotated with `cacheAnnotation({ ttl })` is ever cached. Cache keys default to the runtime's content hash of the plan (`key` overrides), queries inside a transaction or pinned connection bypass the cache, and the default store is an in-memory LRU with TTL (`CacheStore` is the interface for a Redis-style backend).
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { cacheAnnotation, createCacheMiddleware } from '@prisma/orm-extension-middleware-cache';
|
|
205
|
+
|
|
206
|
+
export const db = postgres<Contract>({
|
|
207
|
+
contractJson,
|
|
208
|
+
url: process.env['DATABASE_URL']!,
|
|
209
|
+
middleware: [createCacheMiddleware({ maxEntries: 1_000 }), lints(), budgets({ maxRows: 10_000 })],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Cached for 60s; an identical plan within the TTL is served without a driver call.
|
|
213
|
+
const user = await db.orm.public.User.first({ id: 1 }, (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })));
|
|
214
|
+
// Un-annotated queries always hit the database.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**The cache key carries no identity.** The default key is the runtime's content hash of the plan — contract hash, SQL text, and bound parameters — so two callers issuing the same statement share one entry regardless of who they are. Never annotate a read whose rows depend on the caller (per-user, per-tenant, or RLS-filtered data) on the plain `postgres()` façade unless the identity is part of the key: `cacheAnnotation({ ttl, key: `user:${userId}:profile` })`, or a `where` clause that binds the identity as a parameter (the parameter is in the hash). Queries that run on a pinned connection or inside a transaction bypass the cache entirely (`ctx.scope !== 'runtime'`), which is why a Supabase `RoleBoundDb` read — executed on a connection with the role bound via `set_config` — is never served from cache; the plain façade has no such protection.
|
|
197
218
|
|
|
198
219
|
## Workflow — Compose multiple middleware
|
|
199
220
|
|
|
200
221
|
```typescript
|
|
201
222
|
middleware: [
|
|
202
|
-
|
|
223
|
+
createCacheMiddleware({ maxEntries: 1_000 }), // first — gets first claim on an interceptQuery hit
|
|
203
224
|
lints({ severities: { noLimit: 'error' } }),
|
|
204
|
-
budgets({ maxLatencyMs: 5_000 }),
|
|
225
|
+
budgets({ maxLatencyMs: 5_000 }),
|
|
226
|
+
slowQueryWarning({ thresholdMs: 250 }), // afterQuery fires for cache hits too (source: 'middleware')
|
|
205
227
|
],
|
|
206
228
|
```
|
|
207
229
|
|
|
208
|
-
Order matters:
|
|
230
|
+
Order matters: `beforeQuery` runs in registration order for every middleware before any `interceptQuery` is consulted, so lints and budgets still check a query the cache then answers; `afterQuery` fires for all of them either way.
|
|
209
231
|
|
|
210
232
|
## Workflow — Configure the connection
|
|
211
233
|
|
|
@@ -241,11 +263,11 @@ const isProd = process.env['NODE_ENV'] === 'production';
|
|
|
241
263
|
|
|
242
264
|
export const db = postgres<Contract>({
|
|
243
265
|
contractJson,
|
|
244
|
-
url: process.env['DATABASE_URL']
|
|
266
|
+
url: process.env['DATABASE_URL']!,
|
|
245
267
|
middleware: isProd
|
|
246
|
-
? [
|
|
268
|
+
? [slowQueryWarning({ thresholdMs: 250 })]
|
|
247
269
|
: [
|
|
248
|
-
|
|
270
|
+
slowQueryWarning({ thresholdMs: 250 }),
|
|
249
271
|
lints({ severities: { noLimit: 'error', deleteWithoutWhere: 'error' } }),
|
|
250
272
|
],
|
|
251
273
|
});
|
|
@@ -259,17 +281,17 @@ The concept applies to **Postgres and SQLite**. `db.transaction(fn)` opens a tra
|
|
|
259
281
|
|
|
260
282
|
```typescript
|
|
261
283
|
await db.transaction(async (tx) => {
|
|
262
|
-
const user = await tx.orm.User.create({ email: 'alice@example.com' });
|
|
263
|
-
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
|
|
284
|
+
const user = await tx.orm.public.User.create({ email: 'alice@example.com' });
|
|
285
|
+
await tx.orm.public.Post.create({ userId: user.id, title: 'hello' });
|
|
264
286
|
// If either call throws, both inserts roll back.
|
|
265
287
|
});
|
|
266
288
|
```
|
|
267
289
|
|
|
268
|
-
The callback returns whatever you return from it — the transaction wrapper passes it through. The `tx` object exposes `execute(plan)` for SQL-builder plans inside the transaction.
|
|
290
|
+
The callback returns whatever you return from it — the transaction wrapper passes it through. The `tx` object exposes `query(plan)` (rows) and `execute(plan)` (affected count) for SQL-builder plans inside the transaction.
|
|
269
291
|
|
|
270
292
|
## Workflow — Switch between Postgres, SQLite, and Mongo
|
|
271
293
|
|
|
272
|
-
The concept: the façade selection is baked into `db.ts` (`@internal/postgres` or `@internal/mongo`) and `prisma.config.ts` (which `
|
|
294
|
+
The concept: the façade selection is baked into `db.ts` (`@internal/postgres` or `@internal/mongo`) and `prisma.config.ts` (which target's `config` subpath `ormConfig` is imported from). To switch a project's target, re-run `prisma orm init` in the same directory and pick the other target — the init flow detects the existing scaffold and prompts to reinit (non-interactive runs grant the consent with `--confirm <directory name>`). PN re-scaffolds `prisma.config.ts` and `db.ts` for the new façade. The contract source needs to be re-authored for the new target's idioms (Mongo expresses nested documents; Postgres expresses relations).
|
|
273
295
|
|
|
274
296
|
After the switch (Mongo):
|
|
275
297
|
|
|
@@ -293,7 +315,7 @@ import contractJson from './contract.json' with { type: 'json' };
|
|
|
293
315
|
export const db = sqlite<Contract>({ contractJson, path: 'app.db' });
|
|
294
316
|
```
|
|
295
317
|
|
|
296
|
-
`path` is optional at construct time (you can call `db.connect({ path })` later); omit it and the façade still returns a client. The SQLite façade exposes the same `db.sql`, `db.orm`, `db.transaction(...)`, `db.close()`, and `[Symbol.asyncDispose]` surfaces as Postgres. The Mongo façade shares `db.orm`, `db.close()`, and `[Symbol.asyncDispose]` but has no `db.sql` and no `db.transaction(...)`.
|
|
318
|
+
`path` is optional at construct time (you can call `db.connect({ path })` later); omit it and the façade still returns a client. The SQLite façade exposes the same `db.sql`, `db.orm`, `db.transaction(...)`, `db.close()`, and `[Symbol.asyncDispose]` surfaces as Postgres, with one addressing difference: SQLite has no schemas, so `db.sql` and `db.orm` are the unbound namespace itself (`db.orm.User`, `db.sql.user`) instead of Postgres's `db.orm.public.User` / `db.sql.public.user`. The Mongo façade shares `db.orm`, `db.close()`, and `[Symbol.asyncDispose]` but has no `db.sql` and no `db.transaction(...)`.
|
|
297
319
|
|
|
298
320
|
The `db.sql` / `db.orm` surfaces stay the same in name; the operators each surface exposes are target-shaped (Mongo has no `JOIN`).
|
|
299
321
|
|
|
@@ -311,22 +333,22 @@ The runtime side (this skill) is the same regardless: `db.ts` reads `contract.js
|
|
|
311
333
|
1. **Hardcoding `DATABASE_URL` in `prisma.config.ts`.** Leaks credentials; bypasses per-environment overrides. Use `.env`.
|
|
312
334
|
2. **Omitting the `<Contract>` type parameter** in `postgres<Contract>(...)`. Without it, static surfaces collapse to a generic shape and you lose autocomplete for models. There is no second type parameter — the older two-param signature (`postgres<Contract, TypeMaps>`) is gone.
|
|
313
335
|
3. **Forgetting `with { type: 'json' }` on the contract import.** Required by Node's ESM JSON-import-attribute spec.
|
|
314
|
-
4. **Middleware order matters.**
|
|
315
|
-
5. **Importing middleware from a non-existent
|
|
336
|
+
4. **Middleware order matters.** Registration order is hook order; put the cache first so its `interceptQuery` is consulted first.
|
|
337
|
+
5. **Importing middleware from a non-existent package or subpath.** There is no `@internal/postgres/middleware` subpath and no `@internal/middleware-telemetry` package. `lints` / `budgets` / `SqlMiddleware` come from `@prisma/orm-postgres/family-runtime`; the cache comes from `@prisma/orm-extension-middleware-cache`; a query log or telemetry hook is a custom `afterQuery` middleware (above).
|
|
316
338
|
6. **Confabulating lint / budget option names.** Lints take `severities` (with the five keys above), not `requireWhere` / `maxRowsWithoutLimit`. Budgets use `maxLatencyMs` (not `maxDurationMs`) plus `maxRows` / `defaultTableRows` / `tableRows`. When in doubt, read the source.
|
|
317
339
|
7. **Switching targets without re-emitting.** The contract artefacts are target-shaped; emit after the target change.
|
|
318
340
|
8. **Script hangs after queries finish on Postgres.** The `pg.Pool` keeps Node's event loop alive. Solution: `await db.close()` before the script returns, or `await using db = postgres<Contract>(...)` at the top of a script module. Do not put `await using db = postgres(...)` inside a request handler — it's block-scoped and would close the pool after every request. The right server pattern is a module-level singleton in `db.ts` that lives for the process lifetime.
|
|
319
341
|
|
|
320
342
|
## What Prisma 8 doesn't do yet
|
|
321
343
|
|
|
322
|
-
-
|
|
344
|
+
- **A `/middleware` subpath or a telemetry package.** Neither exists. The middleware surface is `lints`, `budgets`, and the `SqlMiddleware` type on `@prisma/orm-postgres/family-runtime`, plus the separately installed `@prisma/orm-extension-middleware-cache`. Anything else (query log, tracing spans, metrics) is a custom `afterQuery` middleware you write. File additional gaps you hit via `references/feedback.md`.
|
|
323
345
|
- **Multi-database routing / read replicas.** Prisma 8 doesn't ship a built-in primary/replica router or shard-aware client. Workaround: configure separate `db.ts` instances per data store and call the right one in your application code. If you need first-class multi-database routing, file a feature request via the `references/feedback.md` skill.
|
|
324
346
|
- **Connection pooling as a first-class config field.** `poolOptions.connectionTimeoutMillis` and `poolOptions.idleTimeoutMillis` are wired through, but the rest of `pg.Pool`'s tuning surface (max connections, `allowExitOnIdle`, ssl options, …) is not exposed by name. Workaround: construct the `pg.Pool` yourself and pass it via `pg:`. If you need more pool fields surfaced on the façade, file a feature request via the `references/feedback.md` skill.
|
|
325
|
-
- **Query logger middleware as a built-in.** Prisma 8 doesn't ship a "log every query" middleware. Workaround:
|
|
347
|
+
- **Query logger middleware as a built-in.** Prisma 8 doesn't ship a "log every query" middleware. Workaround: a custom `afterQuery` middleware (see *Workflow — Custom middleware*). If you need a built-in query log, file a feature request via the `references/feedback.md` skill.
|
|
326
348
|
|
|
327
349
|
## Reference Files
|
|
328
350
|
|
|
329
|
-
This skill is intentionally body-only; `prisma orm init --help`, the `defineConfig` factory in `packages/3-extensions/postgres/src/config/define-config.ts`, the `postgres()` factory in `packages/3-extensions/postgres/src/runtime/postgres.ts`,
|
|
351
|
+
This skill is intentionally body-only; `prisma orm init --help`, the target `defineConfig` (`ormConfig`) factory in `packages/3-extensions/postgres/src/config/define-config.ts`, the `postgres()` factory in `packages/3-extensions/postgres/src/runtime/postgres.ts`, the middleware sources in `packages/2-sql/5-runtime/src/middleware/{lints,budgets}.ts`, and `packages/3-extensions/middleware-cache/README.md` are the authoritative surfaces for option-level detail. When in doubt, read the source.
|
|
330
352
|
|
|
331
353
|
## Checklist
|
|
332
354
|
|
|
@@ -334,11 +356,11 @@ This skill is intentionally body-only; `prisma orm init --help`, the `defineConf
|
|
|
334
356
|
- [ ] `with { type: 'json' }` on the contract JSON import.
|
|
335
357
|
- [ ] `<Contract>` is the single type parameter on `postgres<Contract>(...)` (no second parameter).
|
|
336
358
|
- [ ] `DATABASE_URL` lives in `.env`, not in `prisma.config.ts`.
|
|
337
|
-
- [ ] Middleware ordered intentionally (
|
|
338
|
-
- [ ] `lints` / `budgets`
|
|
359
|
+
- [ ] Middleware ordered intentionally (cache first when used).
|
|
360
|
+
- [ ] `lints` / `budgets` imported from `@prisma/orm-postgres/family-runtime` and using the verified option keys (`severities`, `maxLatencyMs`, `maxRows`, `tableRows`).
|
|
339
361
|
- [ ] Per-env divergence (if any) gated by `NODE_ENV` or similar.
|
|
340
362
|
- [ ] Did NOT hardcode credentials in any committed file.
|
|
341
|
-
- [ ] Did NOT confabulate a `@internal/postgres/middleware` subpath, a `@internal/postgres-extension-audit` package, or a second type parameter on `postgres<...>`.
|
|
363
|
+
- [ ] Did NOT confabulate a `@internal/postgres/middleware` subpath, a `@internal/middleware-telemetry` package, a `@internal/postgres-extension-audit` package, or a second type parameter on `postgres<...>`.
|
|
342
364
|
- [ ] Did NOT claim `db.transaction(...)` exists on the Mongo façade — only Postgres and SQLite expose it.
|
|
343
365
|
- [ ] Did NOT confabulate read-replica / multi-DB / extra pool config — pointed at *What Prisma 8 doesn't do yet* and routed to `references/feedback.md`.
|
|
344
366
|
- [ ] For build-system / dev-server prompts (Vite plugin, Next.js plugin, …) routed to `references/build.md`.
|
|
@@ -25,7 +25,7 @@ This skill covers using Prisma 8 against a **Supabase** project end-to-end: comp
|
|
|
25
25
|
|
|
26
26
|
- **The pack is an `external` contract space.** `@internal/extension-supabase/pack` ships a complete, introspection-generated contract of everything Supabase owns — the `auth` and `storage` schemas, their native enum types, and the platform roles (`anon`, `authenticated`, `service_role`) — all with control policy `external`. Composed via `extensions`, it means: the migration planner **emits no DDL** for those objects (Supabase manages them), and `db verify` **confirms they exist** in the live database. Your own tables stay `managed` as usual.
|
|
27
27
|
- **Roles come from the pack; you never declare them.** RLS `roles = [authenticated]` identifiers resolve against the composed contract. Pointing the runtime at a non-Supabase Postgres fails verify with a `not-found` issue naming the missing role — the common "wrong database" misconfiguration surfaces before queries run.
|
|
28
|
-
- **The runtime is role-first.** `supabase()` returns a `SupabaseDb` with **no top-level query surface** — there is no `db.sql` / `db.orm` until you bind a role. `await db.asUser(jwt)` / `db.asAnon()` / `db.asServiceRole()` each return a `RoleBoundDb` exposing `.sql`, `.orm`, `.raw`, `.execute(plan)
|
|
28
|
+
- **The runtime is role-first.** `supabase()` returns a `SupabaseDb` with **no top-level query surface** — there is no `db.sql` / `db.orm` until you bind a role. `await db.asUser(jwt)` / `db.asAnon()` / `db.asServiceRole()` each return a `RoleBoundDb` exposing `.sql`, `.orm`, `.raw`, `.query(plan)` (rows), `.execute(plan)` (affected count), and `.transaction(fn)`. This is deliberate: in a Supabase app there is no meaningful "no role" execution context, and defaulting to the connection's login role is a silent-RLS-bypass footgun.
|
|
29
29
|
- **Role binding is below middleware and cannot leak.** Each role-bound query runs on a connection that had `set_config('role', …)` and `set_config('request.jwt.claims', …)` applied beneath the user-middleware chain, with `RESET ALL` on release. Postgres-side `auth.uid()` / `auth.jwt()` read those session vars — RLS enforcement is Postgres's job; the runtime's job is binding the context.
|
|
30
30
|
- **RLS is enforced by policies *and* grants.** Policies filter *rows*; `GRANT` controls *table access*. Prisma 8 authors and migrates the policies; it does not author grants (see *What Prisma 8 doesn't do yet*). A role with policies but no `GRANT` gets a permission error, not filtered rows. On Supabase your `public` tables already carry the platform-role grants via default privileges — the grant that is actually missing out of the box is `service_role`'s on `auth.*` / `storage.*` (see *Workflow — Grants*).
|
|
31
31
|
- **JWT validation is eager and configurable — current Supabase projects need `jwksUrl`.** `asUser(jwt)` verifies the token (via `jose`) *before* any connection is acquired: signature + expiry against `jwksUrl` (asymmetric signing keys — **the default on current Supabase projects**, which sign ES256) **xor** `jwtSecret` (the symmetric HS256 secret — legacy projects only). Both or neither → a structured error with code `SUPABASE.CONFIG_INVALID`. Bad tokens throw a structured error with code `SUPABASE.JWT_INVALID` and a typed `meta.reason` — including a mismatch between the token's algorithm and the configured key source (an ES256 token against a `jwtSecret` client names the problem and tells you to switch to `jwksUrl`). The Postgres role is derived from the token's `role` claim (defaults to `authenticated`). Note: `supabase status` still prints a `JWT_SECRET` even on projects that sign ES256 — its presence does not mean your project uses it.
|
|
@@ -33,32 +33,20 @@ This skill covers using Prisma 8 against a **Supabase** project end-to-end: comp
|
|
|
33
33
|
|
|
34
34
|
## Workflow — Wire the pack into the config
|
|
35
35
|
|
|
36
|
-
The concept: the pack registers the Supabase contract space so your contract can reference it and the planner/verifier know what Supabase owns.
|
|
36
|
+
The concept: the pack registers the Supabase contract space so your contract can reference it and the planner/verifier know what Supabase owns. Its descriptor is the `pack` export and it goes into the ordinary `extensions` array of the target config, inside the standard envelope (see `references/contract.md` § *Key Concepts*). The block mirrors `examples/supabase/prisma.config.ts`:
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
39
|
// prisma.config.ts
|
|
40
|
-
import
|
|
41
|
-
import { defineConfig } from '@internal/cli/config-types';
|
|
42
|
-
import postgresDriver from '@internal/driver-postgres/control';
|
|
40
|
+
import { definePrismaConfig } from '@prisma/cli-engine';
|
|
43
41
|
import supabasePack from '@internal/extension-supabase/pack';
|
|
44
|
-
import
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
family: sql,
|
|
52
|
-
target: postgres,
|
|
53
|
-
adapter: postgresAdapter,
|
|
54
|
-
driver: postgresDriver,
|
|
55
|
-
extensions: [supabasePack],
|
|
56
|
-
contract: prismaContract('./src/contract.prisma', {
|
|
57
|
-
output: 'src/contract.json',
|
|
58
|
-
target: postgresPackRef,
|
|
59
|
-
createNamespace: postgresCreateNamespace,
|
|
42
|
+
import { defineConfig as ormConfig } from '@internal/postgres/config';
|
|
43
|
+
|
|
44
|
+
export default definePrismaConfig({
|
|
45
|
+
orm: ormConfig({
|
|
46
|
+
contract: './src/contract.prisma',
|
|
47
|
+
extensions: [supabasePack],
|
|
48
|
+
migrations: { dir: 'migrations' },
|
|
60
49
|
}),
|
|
61
|
-
migrations: { dir: 'migrations' },
|
|
62
50
|
});
|
|
63
51
|
```
|
|
64
52
|
|
|
@@ -164,10 +152,10 @@ The concept: Supabase-internal tables are not part of your contract, so they are
|
|
|
164
152
|
```typescript
|
|
165
153
|
const admin = db.asServiceRole();
|
|
166
154
|
|
|
167
|
-
// SQL builder over the pack contract
|
|
168
|
-
const users = await admin.supabase
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
// SQL builder over the pack contract — rows come from `query`, not `execute`:
|
|
156
|
+
const users = await admin.supabase.query(
|
|
157
|
+
admin.supabase.sql.auth.users.select('id', 'email').build(),
|
|
158
|
+
);
|
|
171
159
|
|
|
172
160
|
// ORM over the pack contract:
|
|
173
161
|
const sessions = await admin.supabase.orm.auth.AuthSession.select('id', 'aal').all();
|
|
@@ -219,7 +207,6 @@ The concept: the runtime needs a **direct, session-capable** Postgres connection
|
|
|
219
207
|
|
|
220
208
|
## What Prisma 8 doesn't do yet
|
|
221
209
|
|
|
222
|
-
- **No `/control` subpath on the extension** — it can't register through the target façade's `defineConfig({ extensions: [...] })`; wiring goes through the low-level config's `extensions` as shown above. File interest via `references/feedback.md`.
|
|
223
210
|
- **`GRANT` authoring.** Table privileges are not contract elements; the one grant a Supabase app needs (the `service_role` `auth.*` pair for admin reads) is run once by hand (SQL editor / `psql`). If you want grants managed by the contract, file via `references/feedback.md`.
|
|
224
211
|
- **Transactions spanning the app root and the `.supabase` admin root.** The two roots are separate contract-bound runtimes sharing one pool; a cross-root transaction is not supported.
|
|
225
212
|
- **Triggers / functions as contract elements.** The classic "create a profile row on signup" `auth.users` trigger is authored as raw SQL against your database, not in the contract. `auth.uid()` etc. appear only inside opaque policy predicate strings.
|
|
@@ -233,7 +220,7 @@ The concept: the runtime needs a **direct, session-capable** Postgres connection
|
|
|
233
220
|
|
|
234
221
|
## Checklist
|
|
235
222
|
|
|
236
|
-
- [ ] `extensions: [supabasePack]`
|
|
223
|
+
- [ ] `extensions: [supabasePack]` (the `pack` export) in `ormConfig({...})` inside `definePrismaConfig({ orm: ... })`.
|
|
237
224
|
- [ ] Cross-space FK typed `supabase:auth.AuthUser` with explicit `fields` / `references` (+ `onDelete` if wanted).
|
|
238
225
|
- [ ] Every policy target model carries `@@rls`; predicates quote camelCase columns and cast for `auth.uid()`.
|
|
239
226
|
- [ ] `db.ts` uses `await supabase<Contract>({ contractJson, url, jwksUrl | jwtSecret })` — exactly one JWT key source; `jwksUrl` for current projects, `jwtSecret` only for legacy HS256.
|
package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.9-to-8.0.0-rc.10/instructions.md
CHANGED
|
@@ -3,6 +3,7 @@ from: "8.0.0-rc.9"
|
|
|
3
3
|
to: "8.0.0-rc.10"
|
|
4
4
|
# Prisma 8 naming sweep: prose only, no entry required
|
|
5
5
|
# sql-orm-client doc-comment sweep: reviewed, no entry required
|
|
6
|
+
# postgres shell dependency ownership: reviewed, no extension-author action required; bundled packages now declare the catalog Node/pg type dependencies that public shell manifests mirror
|
|
6
7
|
changes:
|
|
7
8
|
- id: to-one-relations-record-nullable
|
|
8
9
|
summary: |
|