@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 +108 -96
- package/dist/index.cjs +68 -112
- package/dist/index.d.cts +95 -31
- package/dist/index.d.mts +95 -31
- package/dist/index.mjs +69 -113
- package/dist/result.cjs +78 -0
- package/dist/result.d.cts +71 -0
- package/dist/result.d.mts +71 -0
- package/dist/result.mjs +73 -0
- package/dist/rls.cjs +28 -28
- package/dist/rls.d.cts +43 -35
- package/dist/rls.d.mts +43 -35
- package/dist/rls.mjs +28 -28
- package/package.json +21 -23
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`,
|
|
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/
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
30
|
-
|
|
31
|
-
// The stand-in for the client YOUR
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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: (
|
|
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
|
|
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
|
|
58
|
+
needs: [Env],
|
|
60
59
|
});
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
**The client type is yours, and that is deliberate.** A Prisma client is
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
port is typed by exactly what it
|
|
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
|
|
79
|
-
folded into the kernel's `GET /healthz` with nothing wired. `/readyz`
|
|
80
|
-
read it: failing readiness on a dependency every replica shares
|
|
81
|
-
all at once.
|
|
82
|
-
- **
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
`
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
a client
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
|
164
|
-
| -------------- |
|
|
165
|
-
| `name` | `prismaDatabase(name)`
|
|
166
|
-
| `client` | `prismaDatabase(name)({ client })`
|
|
167
|
-
| `DATABASE_URL` | environment, read by `prismaDatabase(name)`
|
|
168
|
-
| `setting` | `
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* know the
|
|
16
|
-
*
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* is
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
39
|
-
attributes: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
107
|
-
* `config` provider binding the connection string, the `port`
|
|
108
|
-
* reached through, and the resourceful
|
|
109
|
-
*
|
|
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.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
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
|
};
|