@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/dist/index.d.cts
CHANGED
|
@@ -1,61 +1,125 @@
|
|
|
1
1
|
import { Env } from "@btravstack/config";
|
|
2
|
-
import { HealthChecks,
|
|
2
|
+
import { HealthChecks, Observers } from "@btravstack/core";
|
|
3
3
|
import { Module, PortClassOf } from "@btravstack/di";
|
|
4
|
-
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
30
|
-
* type to ship.
|
|
31
|
-
* the returned type
|
|
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: (
|
|
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
|
|
39
|
-
* `config` provider binding the connection string, the `port`
|
|
40
|
-
* reached through, and the resourceful
|
|
41
|
-
*
|
|
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.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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 |
|
|
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,
|
|
2
|
+
import { HealthChecks, Observers } from "@btravstack/core";
|
|
3
3
|
import { Module, PortClassOf } from "@btravstack/di";
|
|
4
|
-
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
30
|
-
* type to ship.
|
|
31
|
-
* the returned type
|
|
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: (
|
|
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
|
|
39
|
-
* `config` provider binding the connection string, the `port`
|
|
40
|
-
* reached through, and the resourceful
|
|
41
|
-
*
|
|
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.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
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 |
|
|
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,
|
|
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
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* know the
|
|
15
|
-
*
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* is
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
38
|
-
attributes: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
106
|
-
* `config` provider binding the connection string, the `port`
|
|
107
|
-
* reached through, and the resourceful
|
|
108
|
-
*
|
|
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.
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
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
|
};
|
package/dist/result.cjs
ADDED
|
@@ -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;
|