@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/dist/index.d.cts CHANGED
@@ -1,61 +1,125 @@
1
1
  import { Env } from "@btravstack/config";
2
- import { HealthChecks, Instrumentations, Logger, Observers } from "@btravstack/core";
2
+ import { HealthChecks, Observers } from "@btravstack/core";
3
3
  import { Module, PortClassOf } from "@btravstack/di";
4
- import { PrismaPg } from "@prisma/adapter-pg";
4
+ //#region src/instrument.d.ts
5
+ /**
6
+ * A Prisma 8 middleware, structurally — `name`, `familyId` and the four hooks
7
+ * this starter implements.
8
+ *
9
+ * Declared here rather than imported from `@prisma/orm-postgres/family-runtime`
10
+ * so the option's type does not drag the target package into a consumer that
11
+ * only reads this module's types. A real `SqlMiddleware` satisfies it.
12
+ */
13
+ type SqlMiddlewareLike = {
14
+ readonly name: string;
15
+ readonly familyId: "sql";
16
+ readonly beforeQuery: (plan: Plan, ctx: MiddlewareContext) => Promise<void>;
17
+ readonly afterQuery: (plan: Plan, result: Settled, ctx: MiddlewareContext) => Promise<void>;
18
+ readonly beforeExecute: (plan: Plan, ctx: MiddlewareContext) => Promise<void>;
19
+ readonly afterExecute: (plan: Plan, result: Settled, ctx: MiddlewareContext) => Promise<void>;
20
+ };
21
+ /** The rendered statement, which both `before` hooks are handed. */
22
+ type Plan = {
23
+ readonly sql?: string;
24
+ };
25
+ /**
26
+ * What every hook is handed. `planExecutionId` is the runtime's own correlation
27
+ * id — its TSDoc names tracing and timing as the reason it exists — and it is
28
+ * what pairs a `before` with its `after`.
29
+ */
30
+ type MiddlewareContext = {
31
+ readonly planExecutionId: string;
32
+ readonly scope?: "runtime" | "connection" | "transaction";
33
+ };
34
+ /** What an `after` hook reports. There is no error here: Prisma's shape carries none. */
35
+ type Settled = {
36
+ readonly completed: boolean;
37
+ readonly source?: string;
38
+ };
39
+ //#endregion
5
40
  //#region src/prisma.d.ts
6
41
  /**
7
- * All this starter needs of a client: a pool it can close. A generated Prisma
8
- * client satisfies it structurally, and so does an extended one — `$extends`
9
- * preserves `$disconnect` — which is why the application's own client type
10
- * flows through untouched.
42
+ * All this starter needs of a client: a raw lane to probe with, and a runtime
43
+ * to run the probe on and to close.
44
+ *
45
+ * A Prisma 8 client satisfies it structurally. Both members are part of the
46
+ * contract rather than optional because every client has them, and a probe that
47
+ * has to feature-detect its own client cannot report the difference between
48
+ * "the database is down" and "this client cannot be asked".
11
49
  */
12
50
  type PrismaLike = {
13
- readonly $disconnect: () => Promise<void>;
14
51
  /**
15
- * What the health check asks. Part of the contract rather than optional
16
- * because every generated Prisma client has it, and a probe that has to
17
- * feature-detect its own client cannot report the difference between "the
18
- * database is down" and "this client cannot be asked".
52
+ * The raw lane, required to EXIST and deliberately not described further.
53
+ *
54
+ * Spelling its signature out here would refuse every real client: a
55
+ * parameter is contravariant, and the genuine `raw.sql` takes the target's
56
+ * own interpolation union and its own row-spec type, both of which are
57
+ * narrower than anything this package — which cannot see a contract — could
58
+ * name. `unknown` on the property still requires it to be there, which is
59
+ * what makes a client with no raw lane a compile error.
60
+ */
61
+ readonly raw: {
62
+ readonly sql: unknown;
63
+ };
64
+ /**
65
+ * Required to exist, and read through the same cast for the same reason: the
66
+ * real `execute` takes the contract's own plan type.
67
+ */
68
+ readonly runtime: unknown;
69
+ };
70
+ /** What the starter hands {@link PrismaOptions.client}. */
71
+ type PrismaBinding = {
72
+ /** `DATABASE_URL`, read through `Config` rather than by the application. */
73
+ readonly url: string;
74
+ /**
75
+ * The starter's own middleware — one `afterQuery` hook reporting every
76
+ * query to `Observers`. Spread it into the client's `middleware` array
77
+ * beside any of the application's own.
19
78
  */
20
- readonly $queryRaw: (query: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;
79
+ readonly middleware: readonly SqlMiddlewareLike[];
21
80
  };
22
81
  /** What {@link prismaDatabase} is handed. */
23
82
  type PrismaOptions<C extends PrismaLike> = {
24
83
  /**
25
- * Builds the client from the driver adapter, which is already constructed
26
- * from the environment's URL.
84
+ * Builds the client from what the starter bound.
27
85
  *
28
- * This is the one thing the starter cannot own: a Prisma client is
29
- * **generated per application** from its own schema, so there is no client
30
- * type to ship. Applying `@unthrown/prisma`'s extension belongs here too, so
31
- * the returned type is exactly the one the application will hold.
86
+ * This is the one thing the starter cannot own: a Prisma 8 client is typed by
87
+ * the application's own emitted `Contract` and constructed from its own
88
+ * `contract.json`, so there is no client type to ship. Building it here is
89
+ * what makes the returned type exactly the one the application will hold.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * prismaDatabase("OrderDatabase")({
94
+ * client: ({ url, middleware }) =>
95
+ * postgres<Contract>({ contractJson, url, middleware }),
96
+ * });
97
+ * ```
32
98
  */
33
- readonly client: (adapter: PrismaPg) => C;
99
+ readonly client: (binding: PrismaBinding) => C;
34
100
  };
35
101
  /**
36
102
  * A Prisma client whose pool is the application scope's.
37
103
  *
38
- * Returns the three pieces a composition root needs and nothing more: the
39
- * `config` provider binding the connection string, the `port` the client is
40
- * reached through, and the resourceful `provider` that opens it and closes it
41
- * again. Put both providers in a module's `provides` and export the port.
104
+ * Returns a module carrying the three pieces a composition root needs and
105
+ * nothing more: the `config` provider binding the connection string, the `port`
106
+ * the client is reached through, and the resourceful provider that opens it and
107
+ * closes it again.
42
108
  *
43
109
  * **The pool closes on every exit path**, including a boot that fails after
44
110
  * this provider ran — that is what makes it resourceful rather than a plain
45
- * value. `$disconnect` ends the driver adapter's pool without killing the
46
- * client; Prisma dials again lazily on the next statement, which is why no
47
- * spec asserts that a released client refuses to query.
111
+ * value. `runtime().close()` ends the pool; Prisma dials again lazily on the
112
+ * next statement, which is why no spec asserts that a released client refuses
113
+ * to query.
48
114
  *
49
- * **Migrations are not run here.** A deployment runs `prisma migrate deploy`
115
+ * **Migrations are not run here.** A deployment runs `prisma db migrate`
50
116
  * against this same URL *before the process starts*; an application that
51
117
  * migrates itself at boot races every other replica.
52
118
  */
53
- export declare const prismaDatabase: <const N extends string>(name: N) => <C extends PrismaLike>({ client }: PrismaOptions<C>) => Module<HealthChecks | Instrumentations | (PortClassOf<N, C> extends (infer T) ? T extends PortClassOf<N, C> ? T extends import("@btravstack/di").AnyPort ? InstanceType<T> : T extends import("@btravstack/di").AnyProvider ? InstanceType<T["port"]> : never : never : never), import("@btravstack/config").ConfigInvalid, Exclude<Env, HealthChecks | Instrumentations | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
54
- readonly url: string;
55
- }>> | Exclude<Logger, HealthChecks | Instrumentations | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
119
+ export declare const prismaDatabase: <const N extends string>(name: N) => <C extends PrismaLike>({ client }: PrismaOptions<C>) => Module<HealthChecks | (PortClassOf<N, C> extends (infer T) ? T extends PortClassOf<N, C> ? T extends import("@btravstack/di").AnyPort ? InstanceType<T> : T extends import("@btravstack/di").AnyProvider ? InstanceType<T["port"]> : never : never : never), import("@btravstack/config").ConfigInvalid, Exclude<Env, HealthChecks | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
56
120
  readonly url: string;
57
121
  }>>> & {
58
122
  readonly port: PortClassOf<N, C>;
59
123
  };
60
124
  //#endregion
61
- export type { PrismaLike, PrismaOptions };
125
+ export type { PrismaBinding, PrismaLike, PrismaOptions, SqlMiddlewareLike };
package/dist/index.d.mts CHANGED
@@ -1,61 +1,125 @@
1
1
  import { Env } from "@btravstack/config";
2
- import { HealthChecks, Instrumentations, Logger, Observers } from "@btravstack/core";
2
+ import { HealthChecks, Observers } from "@btravstack/core";
3
3
  import { Module, PortClassOf } from "@btravstack/di";
4
- import { PrismaPg } from "@prisma/adapter-pg";
4
+ //#region src/instrument.d.ts
5
+ /**
6
+ * A Prisma 8 middleware, structurally — `name`, `familyId` and the four hooks
7
+ * this starter implements.
8
+ *
9
+ * Declared here rather than imported from `@prisma/orm-postgres/family-runtime`
10
+ * so the option's type does not drag the target package into a consumer that
11
+ * only reads this module's types. A real `SqlMiddleware` satisfies it.
12
+ */
13
+ type SqlMiddlewareLike = {
14
+ readonly name: string;
15
+ readonly familyId: "sql";
16
+ readonly beforeQuery: (plan: Plan, ctx: MiddlewareContext) => Promise<void>;
17
+ readonly afterQuery: (plan: Plan, result: Settled, ctx: MiddlewareContext) => Promise<void>;
18
+ readonly beforeExecute: (plan: Plan, ctx: MiddlewareContext) => Promise<void>;
19
+ readonly afterExecute: (plan: Plan, result: Settled, ctx: MiddlewareContext) => Promise<void>;
20
+ };
21
+ /** The rendered statement, which both `before` hooks are handed. */
22
+ type Plan = {
23
+ readonly sql?: string;
24
+ };
25
+ /**
26
+ * What every hook is handed. `planExecutionId` is the runtime's own correlation
27
+ * id — its TSDoc names tracing and timing as the reason it exists — and it is
28
+ * what pairs a `before` with its `after`.
29
+ */
30
+ type MiddlewareContext = {
31
+ readonly planExecutionId: string;
32
+ readonly scope?: "runtime" | "connection" | "transaction";
33
+ };
34
+ /** What an `after` hook reports. There is no error here: Prisma's shape carries none. */
35
+ type Settled = {
36
+ readonly completed: boolean;
37
+ readonly source?: string;
38
+ };
39
+ //#endregion
5
40
  //#region src/prisma.d.ts
6
41
  /**
7
- * All this starter needs of a client: a pool it can close. A generated Prisma
8
- * client satisfies it structurally, and so does an extended one — `$extends`
9
- * preserves `$disconnect` — which is why the application's own client type
10
- * flows through untouched.
42
+ * All this starter needs of a client: a raw lane to probe with, and a runtime
43
+ * to run the probe on and to close.
44
+ *
45
+ * A Prisma 8 client satisfies it structurally. Both members are part of the
46
+ * contract rather than optional because every client has them, and a probe that
47
+ * has to feature-detect its own client cannot report the difference between
48
+ * "the database is down" and "this client cannot be asked".
11
49
  */
12
50
  type PrismaLike = {
13
- readonly $disconnect: () => Promise<void>;
14
51
  /**
15
- * What the health check asks. Part of the contract rather than optional
16
- * because every generated Prisma client has it, and a probe that has to
17
- * feature-detect its own client cannot report the difference between "the
18
- * database is down" and "this client cannot be asked".
52
+ * The raw lane, required to EXIST and deliberately not described further.
53
+ *
54
+ * Spelling its signature out here would refuse every real client: a
55
+ * parameter is contravariant, and the genuine `raw.sql` takes the target's
56
+ * own interpolation union and its own row-spec type, both of which are
57
+ * narrower than anything this package — which cannot see a contract — could
58
+ * name. `unknown` on the property still requires it to be there, which is
59
+ * what makes a client with no raw lane a compile error.
60
+ */
61
+ readonly raw: {
62
+ readonly sql: unknown;
63
+ };
64
+ /**
65
+ * Required to exist, and read through the same cast for the same reason: the
66
+ * real `execute` takes the contract's own plan type.
67
+ */
68
+ readonly runtime: unknown;
69
+ };
70
+ /** What the starter hands {@link PrismaOptions.client}. */
71
+ type PrismaBinding = {
72
+ /** `DATABASE_URL`, read through `Config` rather than by the application. */
73
+ readonly url: string;
74
+ /**
75
+ * The starter's own middleware — one `afterQuery` hook reporting every
76
+ * query to `Observers`. Spread it into the client's `middleware` array
77
+ * beside any of the application's own.
19
78
  */
20
- readonly $queryRaw: (query: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;
79
+ readonly middleware: readonly SqlMiddlewareLike[];
21
80
  };
22
81
  /** What {@link prismaDatabase} is handed. */
23
82
  type PrismaOptions<C extends PrismaLike> = {
24
83
  /**
25
- * Builds the client from the driver adapter, which is already constructed
26
- * from the environment's URL.
84
+ * Builds the client from what the starter bound.
27
85
  *
28
- * This is the one thing the starter cannot own: a Prisma client is
29
- * **generated per application** from its own schema, so there is no client
30
- * type to ship. Applying `@unthrown/prisma`'s extension belongs here too, so
31
- * the returned type is exactly the one the application will hold.
86
+ * This is the one thing the starter cannot own: a Prisma 8 client is typed by
87
+ * the application's own emitted `Contract` and constructed from its own
88
+ * `contract.json`, so there is no client type to ship. Building it here is
89
+ * what makes the returned type exactly the one the application will hold.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * prismaDatabase("OrderDatabase")({
94
+ * client: ({ url, middleware }) =>
95
+ * postgres<Contract>({ contractJson, url, middleware }),
96
+ * });
97
+ * ```
32
98
  */
33
- readonly client: (adapter: PrismaPg) => C;
99
+ readonly client: (binding: PrismaBinding) => C;
34
100
  };
35
101
  /**
36
102
  * A Prisma client whose pool is the application scope's.
37
103
  *
38
- * Returns the three pieces a composition root needs and nothing more: the
39
- * `config` provider binding the connection string, the `port` the client is
40
- * reached through, and the resourceful `provider` that opens it and closes it
41
- * again. Put both providers in a module's `provides` and export the port.
104
+ * Returns a module carrying the three pieces a composition root needs and
105
+ * nothing more: the `config` provider binding the connection string, the `port`
106
+ * the client is reached through, and the resourceful provider that opens it and
107
+ * closes it again.
42
108
  *
43
109
  * **The pool closes on every exit path**, including a boot that fails after
44
110
  * this provider ran — that is what makes it resourceful rather than a plain
45
- * value. `$disconnect` ends the driver adapter's pool without killing the
46
- * client; Prisma dials again lazily on the next statement, which is why no
47
- * spec asserts that a released client refuses to query.
111
+ * value. `runtime().close()` ends the pool; Prisma dials again lazily on the
112
+ * next statement, which is why no spec asserts that a released client refuses
113
+ * to query.
48
114
  *
49
- * **Migrations are not run here.** A deployment runs `prisma migrate deploy`
115
+ * **Migrations are not run here.** A deployment runs `prisma db migrate`
50
116
  * against this same URL *before the process starts*; an application that
51
117
  * migrates itself at boot races every other replica.
52
118
  */
53
- export declare const prismaDatabase: <const N extends string>(name: N) => <C extends PrismaLike>({ client }: PrismaOptions<C>) => Module<HealthChecks | Instrumentations | (PortClassOf<N, C> extends (infer T) ? T extends PortClassOf<N, C> ? T extends import("@btravstack/di").AnyPort ? InstanceType<T> : T extends import("@btravstack/di").AnyProvider ? InstanceType<T["port"]> : never : never : never), import("@btravstack/config").ConfigInvalid, Exclude<Env, HealthChecks | Instrumentations | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
54
- readonly url: string;
55
- }>> | Exclude<Logger, HealthChecks | Instrumentations | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
119
+ export declare const prismaDatabase: <const N extends string>(name: N) => <C extends PrismaLike>({ client }: PrismaOptions<C>) => Module<HealthChecks | (PortClassOf<N, C> extends (infer T) ? T extends PortClassOf<N, C> ? T extends import("@btravstack/di").AnyPort ? InstanceType<T> : T extends import("@btravstack/di").AnyProvider ? InstanceType<T["port"]> : never : never : never), import("@btravstack/config").ConfigInvalid, Exclude<Env, HealthChecks | Observers | import("@btravstack/di").PortInstance<string, C> | import("@btravstack/di").PortInstance<`${N}Config`, {
56
120
  readonly url: string;
57
121
  }>>> & {
58
122
  readonly port: PortClassOf<N, C>;
59
123
  };
60
124
  //#endregion
61
- export type { PrismaLike, PrismaOptions };
125
+ export type { PrismaBinding, PrismaLike, PrismaOptions, SqlMiddlewareLike };
package/dist/index.mjs CHANGED
@@ -1,150 +1,111 @@
1
1
  import { Config, Env } from "@btravstack/config";
2
- import { HealthCheckFailed, HealthChecks, Instrumentations, Logger, Observers, noObserver, observe } from "@btravstack/core";
2
+ import { HealthCheckFailed, HealthChecks, Observers, noObserver, observe } from "@btravstack/core";
3
3
  import { Module, Port, Provider } from "@btravstack/di";
4
- import { PrismaPg } from "@prisma/adapter-pg";
5
4
  import { fromPromise, fromSafePromise } from "unthrown";
6
5
  //#region src/instrument.ts
7
6
  /**
8
- * Every query, handed to the graph's observers: a count of how it came out, and
9
- * a log line if it failed.
7
+ * Every query, handed to the graph's observers: a span that spans the query, a
8
+ * count of how it came out, and a log line if it failed.
10
9
  *
11
- * **This is why a generated client can be instrumented at all.** Prisma's
12
- * `$extends` takes a `query` component, and `$allModels.$allOperations`
13
- * intercepts every operation on every model — so the wrapper never needs to
14
- * know the schema, which is the thing this package cannot see. An earlier
15
- * revision of this package claimed instrumentation was impossible for that
16
- * reason; it was wrong, and this is the mechanism it missed.
10
+ * **This is why a client the starter cannot name can be instrumented at all.**
11
+ * `middleware` is a construction option rather than a wrapper, and a hook sees
12
+ * every statement on every lane — the ORM's, the SQL builder's and the raw
13
+ * one — so it never needs to know the contract, which is the thing this package
14
+ * cannot see.
17
15
  *
18
- * **It deliberately opens no span.** Prisma's own `@prisma/instrumentation`,
19
- * which this package offers to `Instrumentations` and
20
- * `@btravstack/observability/otel` enables, traces at the ENGINE level — the
21
- * real SQL, the connection acquisition, the serialisation — all of it below
22
- * what a client-level wrapper can see. Emitting a span here as well would put two
23
- * spans on every query for strictly less information. What this wrapper keeps
24
- * is the pair Prisma's instrumentation does not do at all: a metric, and an
25
- * error line correlated with the ambient unit.
16
+ * **It starts the operation in `beforeQuery` and settles it in `afterQuery`**,
17
+ * paired by `ctx.planExecutionId`, because that is what `Observers` is for: the
18
+ * port is called at the START and answers a finisher, so an observer can open a
19
+ * span the query runs *inside*. Settling a span reconstructed afterwards from a
20
+ * duration would make it the parent of nothing. It is also why the runtime's
21
+ * own `latencyMs` is not reported — the observer started the operation, so the
22
+ * duration is its own measurement rather than something to pass along.
26
23
  *
27
- * The wrapper is transparent to the answer: whatever the query resolves or
28
- * rejects with is what the caller receives, which is the kernel's own `RunUnit`
29
- * rule one layer down. `Promise.reject` re-raises rather than `throw`, so the
30
- * rejection propagates to `@unthrown/prisma`'s `try*` twin without this file
31
- * needing a `no-throw` exemption.
24
+ * **Both lanes, because there are two.** A read settles through
25
+ * `afterQuery`; a write with no `RETURNING` — a SQL-builder `delete()` run
26
+ * through `runtime().execute(plan)` — settles through `afterExecute` and would
27
+ * otherwise go unobserved entirely.
28
+ *
29
+ * `completed` is what settles the outcome, and a failed statement does reach
30
+ * the hook: measured against a real database, a duplicate insert arrives as
31
+ * `afterQuery … completed=false` before the rejection surfaces. That is what
32
+ * keeps the errors half of RED honest.
32
33
  */
33
- const instrument = (client, observers) => {
34
- return client.$extends({ query: { $allModels: { $allOperations: ({ model, operation, args, query }) => {
35
- const settle = observe(observers, {
34
+ const queryObserver = (observers) => {
35
+ const open = /* @__PURE__ */ new Map();
36
+ const start = (plan, ctx) => {
37
+ open.set(ctx.planExecutionId, observe(observers, {
36
38
  component: "database",
37
- name: operation,
38
- attributes: {
39
- model: model ?? "raw",
40
- operation
41
- },
42
- traced: false
43
- });
44
- return query(args).then((value) => {
45
- settle({ outcome: "ok" });
46
- return value;
47
- }, (cause) => {
48
- settle({
49
- outcome: "error",
50
- cause
51
- });
52
- return Promise.reject(cause);
39
+ name: "query",
40
+ attributes: { scope: ctx.scope ?? "runtime" },
41
+ ...plan.sql === void 0 ? {} : { details: { sql: plan.sql } }
42
+ }));
43
+ return Promise.resolve();
44
+ };
45
+ const finish = (_plan, result, ctx) => {
46
+ const settle = open.get(ctx.planExecutionId);
47
+ open.delete(ctx.planExecutionId);
48
+ settle?.({
49
+ outcome: result.completed ? "ok" : "error",
50
+ ...result.source === void 0 ? {} : { attributes: { source: result.source } }
53
51
  });
54
- } } } });
55
- };
56
- //#endregion
57
- //#region src/tracing.ts
58
- /** Injected so a spec can drive the arm where the optional peer is absent. */
59
- const defaultLoader = async () => (await import("@prisma/instrumentation")).PrismaInstrumentation;
60
- /**
61
- * Prisma's own OpenTelemetry instrumentation, loaded if it is installed.
62
- *
63
- * Handed to `@btravstack/observability/otel` through the `Instrumentations`
64
- * set port rather than registered here: a graph with no SDK collects nothing
65
- * and this is never called, so engine tracing follows from composing `otel()`
66
- * instead of from importing this package.
67
- *
68
- * **A dynamic import, and it has to be**: `@prisma/instrumentation` is an
69
- * OPTIONAL peer, so a static import would make every consumer install it. The
70
- * import is attempted once, when the instrumented client is built, and a
71
- * failure to resolve is an ordinary answer rather than a fault — an application
72
- * that never wanted engine tracing simply does not have the package.
73
- *
74
- * **This can be a provider at all** because `@prisma/instrumentation` patches no
75
- * modules: `enable()` sets a helper on `globalThis` under a versioned key, and
76
- * a Prisma client reads it PER QUERY. Registration order is therefore free, and
77
- * this works after `@prisma/client` is imported and after clients are built.
78
- * The `--import` preload rule in `@btravstack/observability` governs
79
- * instrumentations that patch, and does not reach this one.
80
- *
81
- * **The skip is logged, not silent.** Telemetry you believe you have and do not
82
- * is worse than none, so the one path that quietly produces no spans says so at
83
- * `debug` — the level for something that is a choice rather than a fault.
84
- *
85
- * This is the one place in this package that still holds a `Logger`, and it is
86
- * why the module needs one: it is a STARTUP fact rather than an operation, so
87
- * the `Observers` seam has nothing to settle. An observer writes lines about
88
- * operations that fail; a missing optional peer is neither.
89
- */
90
- const loadPrismaInstrumentation = async (logger, load = defaultLoader) => {
91
- try {
92
- const instrumentation = new (await (load()))();
93
- instrumentation.enable();
94
- return instrumentation;
95
- } catch {
96
- logger.debug("engine-level tracing is off: @prisma/instrumentation is not installed. Queries are still counted and failures still logged.");
97
- return;
98
- }
52
+ return Promise.resolve();
53
+ };
54
+ return {
55
+ name: "btravstack-observers",
56
+ familyId: "sql",
57
+ beforeQuery: start,
58
+ afterQuery: finish,
59
+ beforeExecute: start,
60
+ afterExecute: finish
61
+ };
99
62
  };
100
63
  //#endregion
101
64
  //#region src/prisma.ts
65
+ /** The one place the cast above is spelled. */
66
+ const probeable = (db) => db;
102
67
  /**
103
68
  * A Prisma client whose pool is the application scope's.
104
69
  *
105
- * Returns the three pieces a composition root needs and nothing more: the
106
- * `config` provider binding the connection string, the `port` the client is
107
- * reached through, and the resourceful `provider` that opens it and closes it
108
- * again. Put both providers in a module's `provides` and export the port.
70
+ * Returns a module carrying the three pieces a composition root needs and
71
+ * nothing more: the `config` provider binding the connection string, the `port`
72
+ * the client is reached through, and the resourceful provider that opens it and
73
+ * closes it again.
109
74
  *
110
75
  * **The pool closes on every exit path**, including a boot that fails after
111
76
  * this provider ran — that is what makes it resourceful rather than a plain
112
- * value. `$disconnect` ends the driver adapter's pool without killing the
113
- * client; Prisma dials again lazily on the next statement, which is why no
114
- * spec asserts that a released client refuses to query.
77
+ * value. `runtime().close()` ends the pool; Prisma dials again lazily on the
78
+ * next statement, which is why no spec asserts that a released client refuses
79
+ * to query.
115
80
  *
116
- * **Migrations are not run here.** A deployment runs `prisma migrate deploy`
81
+ * **Migrations are not run here.** A deployment runs `prisma db migrate`
117
82
  * against this same URL *before the process starts*; an application that
118
83
  * migrates itself at boot races every other replica.
119
84
  */
120
85
  const prismaDatabase = (name) => ({ client }) => {
121
86
  const config = Config.provider(`${name}Config`)(Config.object({ url: Config.string("DATABASE_URL") }));
122
87
  const DatabasePort = Port(name);
123
- const open = (url) => client(new PrismaPg({ connectionString: url }));
124
88
  const port = DatabasePort;
125
89
  const clientProvider = Provider(port)({
126
90
  inject: {
127
91
  settings: config.port,
128
92
  observers: Observers
129
93
  },
130
- acquire: ({ settings, observers }) => {
131
- return fromSafePromise(Promise.resolve(instrument(open(settings.url), observers)));
132
- },
133
- release: (db) => db.$disconnect()
94
+ acquire: ({ settings, observers }) => fromSafePromise(Promise.resolve().then(() => client({
95
+ url: settings.url,
96
+ middleware: [queryObserver(observers)]
97
+ }))),
98
+ release: (db) => probeable(db).runtime().close()
134
99
  });
135
100
  const healthCheck = Provider.member(HealthChecks)({
136
101
  inject: { db: port },
137
102
  sync: ({ db }) => ({
138
103
  name,
139
- check: () => fromPromise(db.$queryRaw`SELECT 1`, (cause) => new HealthCheckFailed({ reason: cause instanceof Error ? cause.message : "database unreachable" })).map(() => void 0)
104
+ check: () => fromPromise(probeable(db).runtime().query(probeable(db).raw.sql`SELECT 1`.affectedCount().build()), (cause) => new HealthCheckFailed({ reason: cause instanceof Error ? cause.message : "database unreachable" })).map(() => void 0)
140
105
  })
141
106
  });
142
- const instrumentation = Provider.member(Instrumentations)({
143
- inject: { logger: Logger },
144
- sync: ({ logger }) => () => loadPrismaInstrumentation(logger)
145
- });
146
107
  const database = Module(name)({
147
- needs: [Env, Logger],
108
+ needs: [Env],
148
109
  provides: [
149
110
  config,
150
111
  Provider.member(Observers)({
@@ -152,14 +113,9 @@ const prismaDatabase = (name) => ({ client }) => {
152
113
  value: noObserver
153
114
  }),
154
115
  clientProvider,
155
- healthCheck,
156
- instrumentation
116
+ healthCheck
157
117
  ],
158
- exports: [
159
- DatabasePort,
160
- HealthChecks,
161
- Instrumentations
162
- ]
118
+ exports: [DatabasePort, HealthChecks]
163
119
  });
164
120
  return Object.assign(database, { port: DatabasePort });
165
121
  };
@@ -0,0 +1,78 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let unthrown = require("unthrown");
3
+ //#region src/result.ts
4
+ /**
5
+ * A write refused by a unique index or constraint — SQLSTATE `23505`.
6
+ *
7
+ * `constraint` is the index the database named, which is what makes this
8
+ * translatable: a table with two unique constraints produces one error type and
9
+ * the caller still knows which one fired.
10
+ */
11
+ var UniqueConstraintViolation = class extends (0, unthrown.TaggedError)("UniqueConstraintViolation") {};
12
+ /** A write refused by a foreign key — SQLSTATE `23503`. */
13
+ var ForeignKeyViolation = class extends (0, unthrown.TaggedError)("ForeignKeyViolation") {};
14
+ /**
15
+ * The database refused the statement outright — SQLSTATE `42501`.
16
+ *
17
+ * On a table with row-level security this is what a `WITH CHECK` violation
18
+ * looks like — an `INSERT` or an `UPDATE` whose resulting row falls outside the
19
+ * policy. It is **not** what every cross-tenant statement produces: `USING`
20
+ * FILTERS, so a `SELECT`, `UPDATE` or `DELETE` that matches another tenant's
21
+ * rows answers nothing at all rather than failing. Only a write that lands
22
+ * outside the policy raises this.
23
+ *
24
+ * It is modeled rather than left as a defect because only the caller knows
25
+ * whether reaching it is a bug (an adapter bound to one tenant) or a request
26
+ * (a caller asking for something it may not have).
27
+ */
28
+ var NotAuthorized = class extends (0, unthrown.TaggedError)("NotAuthorized") {};
29
+ /**
30
+ * SQLSTATE → a tagged error, or the caller's defect for anything unmodeled.
31
+ *
32
+ * The three arms are the ones an application triages. Everything else — a
33
+ * syntax error, a dead connection, a deadlock — is infrastructure a caller
34
+ * cannot act on differently, and belongs on the defect channel where an
35
+ * unexpected failure already goes.
36
+ */
37
+ const qualify = (cause, defect) => {
38
+ const error = cause;
39
+ switch (error?.sqlState) {
40
+ case "23505": return new UniqueConstraintViolation({
41
+ constraint: error.constraint,
42
+ table: error.table
43
+ });
44
+ case "23503": return new ForeignKeyViolation({
45
+ constraint: error.constraint,
46
+ table: error.table
47
+ });
48
+ case "42501": return new NotAuthorized({ table: error.table });
49
+ default: return defect(cause);
50
+ }
51
+ };
52
+ /**
53
+ * Runs a query and answers a `Result` instead of rejecting.
54
+ *
55
+ * It takes a **thunk**, not a promise: an `AsyncResult` is eager, and a promise
56
+ * built at the call site has already started before the Result exists. Passing
57
+ * the work in unstarted is what keeps a sequence of these a sequence rather
58
+ * than a race.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const save = (order: Order) =>
63
+ * tryQuery(() => db.orm.orders.Order.create({ orderId: order.id })).mapErrCases(
64
+ * (matcher, defect) =>
65
+ * matcher
66
+ * .with(P.tag("UniqueConstraintViolation"), () => new DuplicateOrder({ id: order.id }))
67
+ * .with(P.tag("ForeignKeyViolation"), (violation) => defect(violation))
68
+ * .with(P.tag("NotAuthorized"), (refused) => defect(refused)),
69
+ * );
70
+ * ```
71
+ */
72
+ const tryQuery = (run) => (0, unthrown.fromPromise)(Promise.resolve().then(run), qualify);
73
+ //#endregion
74
+ exports.ForeignKeyViolation = ForeignKeyViolation;
75
+ exports.NotAuthorized = NotAuthorized;
76
+ exports.UniqueConstraintViolation = UniqueConstraintViolation;
77
+ exports.qualify = qualify;
78
+ exports.tryQuery = tryQuery;