@btravstack/prisma 0.15.0 โ†’ 0.16.0

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,69 +1,70 @@
1
1
  # @btravstack/prisma
2
2
 
3
3
  > The Prisma starter for [`@btravstack/core`](https://github.com/btravstack/btravstack):
4
- > `DATABASE_URL` bound through `Config`, the Postgres driver adapter, and a
5
- > client whose pool is the application scope's.
4
+ > `DATABASE_URL` bound through `Config`, per-query observation as middleware,
5
+ > and a client whose pool is the application scope's. Prisma **8**.
6
6
 
7
7
  ๐Ÿ“– **[Documentation](https://btravstack.github.io/btravstack/reference/prisma)** ยท
8
8
  [API Reference](https://btravstack.github.io/btravstack/api/prisma/)
9
9
 
10
10
  ```sh
11
11
  pnpm add @btravstack/prisma @btravstack/core @btravstack/config @btravstack/di unthrown \
12
- @prisma/adapter-pg
12
+ @prisma/orm-postgres@8.0.0-rc.11
13
13
  ```
14
14
 
15
15
  Five peer dependencies โ€” install every one, so the application holds a single
16
- copy of each. Your own `@prisma/client`, `prisma` and (if you want the `try*`
17
- twins) `@unthrown/prisma` are yours, not this package's: the client is
18
- generated from your schema. `@prisma/client` is also an **optional** peer here,
19
- used only by the `@btravstack/prisma/rls` subpath below โ€” a consumer that never
20
- imports it installs nothing extra. Node `>=22`.
16
+ copy of each. The `prisma` CLI is yours, as a dev dependency: the client is
17
+ built from _your_ emitted contract, so there is none for this package to ship.
18
+ Node `>=22`.
19
+
20
+ **This is Prisma 8**, which is a different package family rather than a version
21
+ bump โ€” `@prisma/orm-postgres` replaces `@prisma/client` and `@prisma/adapter-pg`
22
+ both, and there is no `@prisma/instrumentation`. For Prisma 7 use
23
+ `@btravstack/prisma@0.14`.
21
24
 
22
25
  ## A worked example
23
26
 
24
27
  <!-- doctest: group=order-api -->
25
28
  <!-- doctest: prelude
26
29
  import { Env } from "@btravstack/config";
27
- import { Logger } from "@btravstack/core";
28
30
  import { Module } from "@btravstack/di";
29
- import { PrismaPg } from "@prisma/adapter-pg";
30
-
31
- // The stand-in for the client YOUR schema generates, and the extension you
32
- // apply to it. Neither exists in this package โ€” that is what the `client`
33
- // arrow is for.
34
- declare class PrismaClient {
35
- constructor(options: { readonly adapter: PrismaPg });
36
- $disconnect(): Promise<void>;
37
- $queryRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<unknown>;
38
- $extends(extension: unknown): this;
39
- }
40
- declare const unthrownPrisma: unknown;
31
+ import type { PrismaBinding } from "@btravstack/prisma";
32
+
33
+ // The stand-in for the client YOUR contract types. It does not exist in this
34
+ // package โ€” that is what the `client` arrow is for.
35
+ declare const postgres: (options: PrismaBinding & { readonly contractJson: unknown }) => {
36
+ readonly raw: { readonly sql: unknown };
37
+ readonly runtime: unknown;
38
+ };
39
+ declare const contractJson: unknown;
41
40
  -->
42
41
 
43
42
  ```ts
44
43
  import { prismaDatabase } from "@btravstack/prisma";
45
44
 
46
45
  const database = prismaDatabase("OrderDatabase")({
47
- client: (adapter) => new PrismaClient({ adapter }).$extends(unthrownPrisma),
46
+ client: ({ url, middleware }) => postgres({ contractJson, url, middleware }),
48
47
  });
49
48
  ```
50
49
 
51
50
  That is the whole surface. `database` is a **module** carrying the port a
52
51
  composition root reads; the provider binding `DATABASE_URL`, the resourceful
53
- client provider, the health check and the engine-tracing loader are inside it:
52
+ client provider and the health check are inside it:
54
53
 
55
54
  ```ts
56
55
  export const PersistenceModule = Module("Persistence")({
57
56
  imports: [database],
58
57
  exports: [database.port],
59
- needs: [Env, Logger],
58
+ needs: [Env],
60
59
  });
61
60
  ```
62
61
 
63
- **The client type is yours, and that is deliberate.** A Prisma client is
64
- generated from _your_ schema, so there is none for this package to ship โ€” the
65
- `client` arrow is where your generated class and your extensions meet, and the
66
- port is typed by exactly what it returns.
62
+ **The client type is yours, and that is deliberate.** A Prisma 8 client is
63
+ typed by _your_ emitted `Contract` and built from _your_ `contract.json`, so
64
+ there is none for this package to ship โ€” the `client` arrow is where your
65
+ contract and this starter meet, and the port is typed by exactly what it
66
+ returns. Spread the `middleware` it hands you into the client: that is where
67
+ the per-query observation below comes from.
67
68
 
68
69
  ## What it owns
69
70
 
@@ -71,87 +72,98 @@ port is typed by exactly what it returns.
71
72
  `ConfigInvalid` naming the variable, not a throw โ€” so a misconfigured
72
73
  deployment exits `78` with the reason on stderr instead of crashing on the
73
74
  first query.
74
- - **The Postgres driver adapter**, constructed from that URL. Another driver is
75
- reachable โ€” build it in your own `client` arrow and ignore the one passed in.
76
75
  - **The pool's lifetime.** The provider is _resourceful_, so `release` closes it
77
76
  on every exit path, including a boot that fails after it ran.
78
- - **A health check**, named after the starter โ€” `SELECT 1` through `$queryRaw`,
79
- folded into the kernel's `GET /healthz` with nothing wired. `/readyz` does not
80
- read it: failing readiness on a dependency every replica shares removes them
81
- all at once.
82
- - **Engine tracing, offered rather than registered.** The starter contributes a
83
- loader for the optional `@prisma/instrumentation` peer to `Instrumentations`;
84
- composing `@btravstack/observability/otel` is what turns it on, and a graph
85
- with no SDK never imports it. A missing peer is a `debug` line, never silence.
86
- - **Every query handed to `Observers`**, so composing `observability()` writes
87
- the failures as lines and `otel()` mints the instruments โ€” with no flag here
88
- and no port list to satisfy when you compose neither.
77
+ - **A health check**, named after the starter โ€” `SELECT 1` through the raw
78
+ lane, folded into the kernel's `GET /healthz` with nothing wired. `/readyz`
79
+ does not read it: failing readiness on a dependency every replica shares
80
+ removes them all at once.
81
+ - **Every query handed to `Observers`**, as one `afterQuery` middleware, so
82
+ composing `observability()` writes the failures as lines and `otel()` opens
83
+ the spans and mints the instruments โ€” with no flag here and no port list to
84
+ satisfy when you compose neither. A middleware sees the ORM lane, the SQL
85
+ builder and the raw lane alike, and carries the runtime's own `latencyMs`.
86
+
87
+ ## `Result`s, on the `@btravstack/prisma/result` subpath
88
+
89
+ Prisma 8 throws a **structured** error carrying PostgreSQL's own SQLSTATE.
90
+ `tryQuery` turns that into a `Result` with three modeled arms โ€”
91
+ `UniqueConstraintViolation` (`23505`, with the `constraint` the database
92
+ named), `ForeignKeyViolation` (`23503`) and `NotAuthorized` (`42501`,
93
+ including a write a row-security policy refused). Anything else is a defect.
94
+
95
+ <!-- doctest: isolate
96
+ import { tryQuery } from "@btravstack/prisma/result";
97
+ import { P } from "unthrown";
98
+
99
+ declare const db: { readonly orm: { readonly orders: { readonly Order: { readonly create: (row: { readonly orderId: string }) => Promise<unknown> } } } };
100
+ declare const orderId: string;
101
+ declare const duplicate: (id: string) => Error;
102
+ -->
103
+
104
+ ```ts
105
+ const saved = tryQuery(() => db.orm.orders.Order.create({ orderId })).mapErrCases(
106
+ (matcher, defect) =>
107
+ matcher
108
+ .with(P.tag("UniqueConstraintViolation"), () => duplicate(orderId))
109
+ .with(P.tag("ForeignKeyViolation"), P.tag("NotAuthorized"), (cause) => defect(cause)),
110
+ );
111
+ ```
112
+
113
+ It takes a **thunk** rather than a promise: an `AsyncResult` is eager, so a
114
+ promise built at the call site has already started before the Result exists.
89
115
 
90
116
  ## Row-level security, on the `@btravstack/prisma/rls` subpath
91
117
 
92
- `tenantScoped(tenant)` is a Prisma client extension that pins **every** statement
93
- โ€” raw SQL included โ€” to `tenant` through a transaction-local
94
- `set_config('app.tenant_id', tenant, true)`, so a PostgreSQL row-level-security
95
- policy reading `current_setting('app.tenant_id', true)` sees it. Apply it
96
- **last**:
118
+ **The policy is declared in your contract** โ€” `@@rls` on the model and a
119
+ `policy_all` block beside it โ€” and the planner emits `ENABLE ROW LEVEL
120
+ SECURITY` and `CREATE POLICY` like any other migration operation. **The pin is
121
+ a transaction:**
97
122
 
98
123
  <!-- doctest: isolate
99
- // `PrismaClient` and `unthrownPrisma` are the reader's own, exactly as in the
100
- // worked example above โ€” so what this fence holds is `tenantScoped`'s call
101
- // shape and its place in the chain, not the generated client's `$extends`.
102
- // That half is compiled on the reference page, over the real client.
103
- import { PrismaPg } from "@prisma/adapter-pg";
104
-
105
- declare class PrismaClient {
106
- constructor(options: { readonly adapter: PrismaPg });
107
- $extends(extension: unknown): this;
108
- }
109
- declare const adapter: PrismaPg;
110
- declare const unthrownPrisma: unknown;
124
+ import { tenantPinned } from "@btravstack/prisma/rls";
125
+
126
+ declare const db: {
127
+ readonly raw: { readonly sql: unknown };
128
+ readonly transaction: <R>(fn: (tx: { readonly query: (plan: never) => Promise<unknown>; readonly orm: { readonly orders: { readonly Order: { readonly all: () => Promise<readonly unknown[]> } } } }) => PromiseLike<R>) => Promise<R>;
129
+ };
111
130
  declare const tenant: string;
112
131
  -->
113
132
 
114
133
  ```ts
115
- import { tenantScoped } from "@btravstack/prisma/rls";
116
-
117
- const db = new PrismaClient({ adapter }).$extends(unthrownPrisma).$extends(tenantScoped(tenant));
134
+ const orders = await tenantPinned(db, tenant, (tx) => tx.orm.orders.Order.all());
118
135
  ```
119
136
 
120
- That fence compiles `tenantScoped`'s call shape and its place in the chain over
121
- a client declared for the sample; the fence on the
122
- [reference page](https://btravstack.github.io/btravstack/reference/prisma) is
123
- the one compiled over a real generated client, which is what proves the
124
- extension is assignable to its `$extends`.
125
-
126
- It goes last because a transaction callback's `tx` comes from the client as it
127
- stood when this extension was applied, so anything added after it is invisible
128
- inside a transaction. `$transaction([...])` is refused rather than silently pinned: the
129
- batch form stops being atomic under this design, and a rejected promise beats a
130
- transaction that quietly no longer rolls back. Use the callback form.
131
-
132
- The database half stays the deployment's, and every part of it fails quietly
133
- when forgotten: a **superuser** role bypasses row security whatever the table
134
- says, and a policy without `FORCE ROW LEVEL SECURITY` is not applied to the
135
- table's **owner** โ€” the role that ran the migrations. Either way the
136
- application works and no tenant is isolated. The policy must also read the
137
- **same** setting `tenantScoped` was given โ€” `app.tenant_id` is only the default
138
- โ€” because a `tenantScoped(tenant, { setting })` and a policy naming a different
139
- one deny every row and every write, which looks exactly like row security
140
- working. The
137
+ `set_config(โ€ฆ, true)` is **transaction-local**, which is why this opens one
138
+ rather than handing back a pinned client: a session-scoped pin works only while
139
+ the pool happens to return the same connection, and fails silently the first
140
+ time it does not. The policy must read the **same** setting โ€” `app.tenant_id`
141
+ is only the default โ€” because a `tenantPinned(db, tenant, work, { setting })`
142
+ and a policy naming a different one deny every row and every write, which looks
143
+ exactly like row security working.
144
+
145
+ Two halves stay the deployment's, and both fail quietly when forgotten: Prisma 8
146
+ authors policies but no `GRANT`s, and it emits `ENABLE ROW LEVEL SECURITY` with
147
+ no way to say `FORCE` โ€” so the table's **owner**, which is the role that ran the
148
+ migrations, bypasses every policy. Connect as a role that is neither the owner
149
+ nor a superuser. The
141
150
  [reference page](https://btravstack.github.io/btravstack/reference/prisma)
142
- carries the DDL and the two smaller lines that fail the same way.
151
+ carries the DDL and the smaller lines that fail the same way.
143
152
 
144
153
  ## What it does not
145
154
 
146
- **Migrations.** A deployment runs `prisma migrate deploy` against the same
147
- database _before the process starts_ โ€” and, where row security is on, as its
148
- **owner** rather than the role `DATABASE_URL` carries. An application that
149
- migrates itself at boot races every other replica.
155
+ **Migrations.** A deployment runs `prisma db migrate` against the same database
156
+ _before the process starts_ โ€” and, where row security is on, as its **owner**
157
+ rather than the role `DATABASE_URL` carries. An application that migrates itself
158
+ at boot races every other replica.
159
+
160
+ **Engine-level tracing.** There is no engine: Prisma 8 is a TypeScript runtime
161
+ and ships no instrumentation package, so the `afterQuery` middleware is the
162
+ whole seam and it already carries the runtime's own latency.
150
163
 
151
164
  **Transactions.** Commit boundaries belong to the adapter, spelled at the call โ€”
152
- `@unthrown/prisma`'s `$tryTransaction` is the primitive. The `rls` subpath
153
- overrides `$transaction` to pin the tenant on the connection and nothing more;
154
- it opens no boundary of its own. There is no unit-scoped
165
+ `db.transaction(fn)` is the primitive. The `rls` subpath opens one transaction
166
+ to pin a setting on and the work inside it is yours. There is no unit-scoped
155
167
  transaction and there will not be one; see
156
168
  [the kernel maps nothing](https://btravstack.github.io/btravstack/explanation/the-kernel-maps-nothing).
157
169
 
@@ -160,12 +172,12 @@ package's.
160
172
 
161
173
  ## Options
162
174
 
163
- | Option | Where | What it is |
164
- | -------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
165
- | `name` | `prismaDatabase(name)` | the port's id and the health check's name โ€” required |
166
- | `client` | `prismaDatabase(name)({ client })` | builds your client from the driver adapter this package built from the URL โ€” required |
167
- | `DATABASE_URL` | environment, read by `prismaDatabase(name)` | the connection string โ€” required, validated at graph build; blank is an error, exit `78` |
168
- | `setting` | `tenantScoped(tenant, { setting })` | the PostgreSQL run-time setting the policy reads โ€” default `app.tenant_id` |
175
+ | Option | Where | What it is |
176
+ | -------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
177
+ | `name` | `prismaDatabase(name)` | the port's id and the health check's name โ€” required |
178
+ | `client` | `prismaDatabase(name)({ client })` | builds your client from the URL and middleware this package bound โ€” required |
179
+ | `DATABASE_URL` | environment, read by `prismaDatabase(name)` | the connection string โ€” required, validated at graph build; blank is an error, exit `78` |
180
+ | `setting` | `tenantPinned(db, tenant, work, { setting })` | the PostgreSQL run-time setting the policy reads โ€” default `app.tenant_id` |
169
181
 
170
182
  There is **no `instrumented` flag**: observation is a set port every call is
171
183
  handed to, so a graph composing no observability pays one inert call and no
package/dist/index.cjs CHANGED
@@ -2,150 +2,111 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _btravstack_config = require("@btravstack/config");
3
3
  let _btravstack_core = require("@btravstack/core");
4
4
  let _btravstack_di = require("@btravstack/di");
5
- let _prisma_adapter_pg = require("@prisma/adapter-pg");
6
5
  let unthrown = require("unthrown");
7
6
  //#region src/instrument.ts
8
7
  /**
9
- * Every query, handed to the graph's observers: a count of how it came out, and
10
- * a log line if it failed.
8
+ * Every query, handed to the graph's observers: a span that spans the query, a
9
+ * count of how it came out, and a log line if it failed.
11
10
  *
12
- * **This is why a generated client can be instrumented at all.** Prisma's
13
- * `$extends` takes a `query` component, and `$allModels.$allOperations`
14
- * intercepts every operation on every model โ€” so the wrapper never needs to
15
- * know the schema, which is the thing this package cannot see. An earlier
16
- * revision of this package claimed instrumentation was impossible for that
17
- * reason; it was wrong, and this is the mechanism it missed.
11
+ * **This is why a client the starter cannot name can be instrumented at all.**
12
+ * `middleware` is a construction option rather than a wrapper, and a hook sees
13
+ * every statement on every lane โ€” the ORM's, the SQL builder's and the raw
14
+ * one โ€” so it never needs to know the contract, which is the thing this package
15
+ * cannot see.
18
16
  *
19
- * **It deliberately opens no span.** Prisma's own `@prisma/instrumentation`,
20
- * which this package offers to `Instrumentations` and
21
- * `@btravstack/observability/otel` enables, traces at the ENGINE level โ€” the
22
- * real SQL, the connection acquisition, the serialisation โ€” all of it below
23
- * what a client-level wrapper can see. Emitting a span here as well would put two
24
- * spans on every query for strictly less information. What this wrapper keeps
25
- * is the pair Prisma's instrumentation does not do at all: a metric, and an
26
- * error line correlated with the ambient unit.
17
+ * **It starts the operation in `beforeQuery` and settles it in `afterQuery`**,
18
+ * paired by `ctx.planExecutionId`, because that is what `Observers` is for: the
19
+ * port is called at the START and answers a finisher, so an observer can open a
20
+ * span the query runs *inside*. Settling a span reconstructed afterwards from a
21
+ * duration would make it the parent of nothing. It is also why the runtime's
22
+ * own `latencyMs` is not reported โ€” the observer started the operation, so the
23
+ * duration is its own measurement rather than something to pass along.
27
24
  *
28
- * The wrapper is transparent to the answer: whatever the query resolves or
29
- * rejects with is what the caller receives, which is the kernel's own `RunUnit`
30
- * rule one layer down. `Promise.reject` re-raises rather than `throw`, so the
31
- * rejection propagates to `@unthrown/prisma`'s `try*` twin without this file
32
- * needing a `no-throw` exemption.
25
+ * **Both lanes, because there are two.** A read settles through
26
+ * `afterQuery`; a write with no `RETURNING` โ€” a SQL-builder `delete()` run
27
+ * through `runtime().execute(plan)` โ€” settles through `afterExecute` and would
28
+ * otherwise go unobserved entirely.
29
+ *
30
+ * `completed` is what settles the outcome, and a failed statement does reach
31
+ * the hook: measured against a real database, a duplicate insert arrives as
32
+ * `afterQuery โ€ฆ completed=false` before the rejection surfaces. That is what
33
+ * keeps the errors half of RED honest.
33
34
  */
34
- const instrument = (client, observers) => {
35
- return client.$extends({ query: { $allModels: { $allOperations: ({ model, operation, args, query }) => {
36
- const settle = (0, _btravstack_core.observe)(observers, {
35
+ const queryObserver = (observers) => {
36
+ const open = /* @__PURE__ */ new Map();
37
+ const start = (plan, ctx) => {
38
+ open.set(ctx.planExecutionId, (0, _btravstack_core.observe)(observers, {
37
39
  component: "database",
38
- name: operation,
39
- attributes: {
40
- model: model ?? "raw",
41
- operation
42
- },
43
- traced: false
44
- });
45
- return query(args).then((value) => {
46
- settle({ outcome: "ok" });
47
- return value;
48
- }, (cause) => {
49
- settle({
50
- outcome: "error",
51
- cause
52
- });
53
- return Promise.reject(cause);
40
+ name: "query",
41
+ attributes: { scope: ctx.scope ?? "runtime" },
42
+ ...plan.sql === void 0 ? {} : { details: { sql: plan.sql } }
43
+ }));
44
+ return Promise.resolve();
45
+ };
46
+ const finish = (_plan, result, ctx) => {
47
+ const settle = open.get(ctx.planExecutionId);
48
+ open.delete(ctx.planExecutionId);
49
+ settle?.({
50
+ outcome: result.completed ? "ok" : "error",
51
+ ...result.source === void 0 ? {} : { attributes: { source: result.source } }
54
52
  });
55
- } } } });
56
- };
57
- //#endregion
58
- //#region src/tracing.ts
59
- /** Injected so a spec can drive the arm where the optional peer is absent. */
60
- const defaultLoader = async () => (await import("@prisma/instrumentation")).PrismaInstrumentation;
61
- /**
62
- * Prisma's own OpenTelemetry instrumentation, loaded if it is installed.
63
- *
64
- * Handed to `@btravstack/observability/otel` through the `Instrumentations`
65
- * set port rather than registered here: a graph with no SDK collects nothing
66
- * and this is never called, so engine tracing follows from composing `otel()`
67
- * instead of from importing this package.
68
- *
69
- * **A dynamic import, and it has to be**: `@prisma/instrumentation` is an
70
- * OPTIONAL peer, so a static import would make every consumer install it. The
71
- * import is attempted once, when the instrumented client is built, and a
72
- * failure to resolve is an ordinary answer rather than a fault โ€” an application
73
- * that never wanted engine tracing simply does not have the package.
74
- *
75
- * **This can be a provider at all** because `@prisma/instrumentation` patches no
76
- * modules: `enable()` sets a helper on `globalThis` under a versioned key, and
77
- * a Prisma client reads it PER QUERY. Registration order is therefore free, and
78
- * this works after `@prisma/client` is imported and after clients are built.
79
- * The `--import` preload rule in `@btravstack/observability` governs
80
- * instrumentations that patch, and does not reach this one.
81
- *
82
- * **The skip is logged, not silent.** Telemetry you believe you have and do not
83
- * is worse than none, so the one path that quietly produces no spans says so at
84
- * `debug` โ€” the level for something that is a choice rather than a fault.
85
- *
86
- * This is the one place in this package that still holds a `Logger`, and it is
87
- * why the module needs one: it is a STARTUP fact rather than an operation, so
88
- * the `Observers` seam has nothing to settle. An observer writes lines about
89
- * operations that fail; a missing optional peer is neither.
90
- */
91
- const loadPrismaInstrumentation = async (logger, load = defaultLoader) => {
92
- try {
93
- const instrumentation = new (await (load()))();
94
- instrumentation.enable();
95
- return instrumentation;
96
- } catch {
97
- logger.debug("engine-level tracing is off: @prisma/instrumentation is not installed. Queries are still counted and failures still logged.");
98
- return;
99
- }
53
+ return Promise.resolve();
54
+ };
55
+ return {
56
+ name: "btravstack-observers",
57
+ familyId: "sql",
58
+ beforeQuery: start,
59
+ afterQuery: finish,
60
+ beforeExecute: start,
61
+ afterExecute: finish
62
+ };
100
63
  };
101
64
  //#endregion
102
65
  //#region src/prisma.ts
66
+ /** The one place the cast above is spelled. */
67
+ const probeable = (db) => db;
103
68
  /**
104
69
  * A Prisma client whose pool is the application scope's.
105
70
  *
106
- * Returns the three pieces a composition root needs and nothing more: the
107
- * `config` provider binding the connection string, the `port` the client is
108
- * reached through, and the resourceful `provider` that opens it and closes it
109
- * again. Put both providers in a module's `provides` and export the port.
71
+ * Returns a module carrying the three pieces a composition root needs and
72
+ * nothing more: the `config` provider binding the connection string, the `port`
73
+ * the client is reached through, and the resourceful provider that opens it and
74
+ * closes it again.
110
75
  *
111
76
  * **The pool closes on every exit path**, including a boot that fails after
112
77
  * this provider ran โ€” that is what makes it resourceful rather than a plain
113
- * value. `$disconnect` ends the driver adapter's pool without killing the
114
- * client; Prisma dials again lazily on the next statement, which is why no
115
- * spec asserts that a released client refuses to query.
78
+ * value. `runtime().close()` ends the pool; Prisma dials again lazily on the
79
+ * next statement, which is why no spec asserts that a released client refuses
80
+ * to query.
116
81
  *
117
- * **Migrations are not run here.** A deployment runs `prisma migrate deploy`
82
+ * **Migrations are not run here.** A deployment runs `prisma db migrate`
118
83
  * against this same URL *before the process starts*; an application that
119
84
  * migrates itself at boot races every other replica.
120
85
  */
121
86
  const prismaDatabase = (name) => ({ client }) => {
122
87
  const config = _btravstack_config.Config.provider(`${name}Config`)(_btravstack_config.Config.object({ url: _btravstack_config.Config.string("DATABASE_URL") }));
123
88
  const DatabasePort = (0, _btravstack_di.Port)(name);
124
- const open = (url) => client(new _prisma_adapter_pg.PrismaPg({ connectionString: url }));
125
89
  const port = DatabasePort;
126
90
  const clientProvider = (0, _btravstack_di.Provider)(port)({
127
91
  inject: {
128
92
  settings: config.port,
129
93
  observers: _btravstack_core.Observers
130
94
  },
131
- acquire: ({ settings, observers }) => {
132
- return (0, unthrown.fromSafePromise)(Promise.resolve(instrument(open(settings.url), observers)));
133
- },
134
- release: (db) => db.$disconnect()
95
+ acquire: ({ settings, observers }) => (0, unthrown.fromSafePromise)(Promise.resolve().then(() => client({
96
+ url: settings.url,
97
+ middleware: [queryObserver(observers)]
98
+ }))),
99
+ release: (db) => probeable(db).runtime().close()
135
100
  });
136
101
  const healthCheck = _btravstack_di.Provider.member(_btravstack_core.HealthChecks)({
137
102
  inject: { db: port },
138
103
  sync: ({ db }) => ({
139
104
  name,
140
- check: () => (0, unthrown.fromPromise)(db.$queryRaw`SELECT 1`, (cause) => new _btravstack_core.HealthCheckFailed({ reason: cause instanceof Error ? cause.message : "database unreachable" })).map(() => void 0)
105
+ check: () => (0, unthrown.fromPromise)(probeable(db).runtime().query(probeable(db).raw.sql`SELECT 1`.affectedCount().build()), (cause) => new _btravstack_core.HealthCheckFailed({ reason: cause instanceof Error ? cause.message : "database unreachable" })).map(() => void 0)
141
106
  })
142
107
  });
143
- const instrumentation = _btravstack_di.Provider.member(_btravstack_core.Instrumentations)({
144
- inject: { logger: _btravstack_core.Logger },
145
- sync: ({ logger }) => () => loadPrismaInstrumentation(logger)
146
- });
147
108
  const database = (0, _btravstack_di.Module)(name)({
148
- needs: [_btravstack_config.Env, _btravstack_core.Logger],
109
+ needs: [_btravstack_config.Env],
149
110
  provides: [
150
111
  config,
151
112
  _btravstack_di.Provider.member(_btravstack_core.Observers)({
@@ -153,14 +114,9 @@ const prismaDatabase = (name) => ({ client }) => {
153
114
  value: _btravstack_core.noObserver
154
115
  }),
155
116
  clientProvider,
156
- healthCheck,
157
- instrumentation
117
+ healthCheck
158
118
  ],
159
- exports: [
160
- DatabasePort,
161
- _btravstack_core.HealthChecks,
162
- _btravstack_core.Instrumentations
163
- ]
119
+ exports: [DatabasePort, _btravstack_core.HealthChecks]
164
120
  });
165
121
  return Object.assign(database, { port: DatabasePort });
166
122
  };