@thomasfosterau/effect-drizzle-ext 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +508 -2
- package/dist/columns.d.ts +113 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/columns.js +146 -0
- package/dist/columns.js.map +1 -0
- package/dist/constraints.d.ts +117 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +93 -0
- package/dist/constraints.js.map +1 -0
- package/dist/database.d.ts +189 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +51 -0
- package/dist/database.js.map +1 -0
- package/dist/derive.d.ts +97 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +142 -0
- package/dist/derive.js.map +1 -0
- package/dist/errors.d.ts +157 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +287 -0
- package/dist/errors.js.map +1 -0
- package/dist/index-CFhgcF4N.d.ts +418 -0
- package/dist/index-CFhgcF4N.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/jsonapi/derive.d.ts +28 -0
- package/dist/jsonapi/derive.d.ts.map +1 -0
- package/dist/jsonapi/derive.js +28 -0
- package/dist/jsonapi/derive.js.map +1 -0
- package/dist/jsonapi/index.d.ts +189 -0
- package/dist/jsonapi/index.d.ts.map +1 -0
- package/dist/jsonapi/index.js +164 -0
- package/dist/jsonapi/index.js.map +1 -0
- package/dist/query.d.ts +111 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +142 -0
- package/dist/query.js.map +1 -0
- package/dist/references.d.ts +38 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +60 -0
- package/dist/references.js.map +1 -0
- package/dist/sort.d.ts +129 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +225 -0
- package/dist/sort.js.map +1 -0
- package/dist/src-CBeuhy5b.js +478 -0
- package/dist/src-CBeuhy5b.js.map +1 -0
- package/dist/update.d.ts +61 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +64 -0
- package/dist/update.js.map +1 -0
- package/dist/versioned-chain.d.ts +524 -0
- package/dist/versioned-chain.d.ts.map +1 -0
- package/dist/versioned-chain.js +298 -0
- package/dist/versioned-chain.js.map +1 -0
- package/package.json +109 -3
- package/src/columns.ts +186 -0
- package/src/constraints.ts +225 -0
- package/src/database.ts +273 -0
- package/src/derivation-error.ts +27 -0
- package/src/derive.ts +320 -0
- package/src/errors.ts +466 -0
- package/src/index.ts +653 -0
- package/src/jsonapi/derive.ts +74 -0
- package/src/jsonapi/index.ts +355 -0
- package/src/query.ts +183 -0
- package/src/references.ts +103 -0
- package/src/sort.ts +368 -0
- package/src/tree.ts +367 -0
- package/src/update.ts +88 -0
- package/src/versioned-chain.ts +1027 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { PgTransactionConfig } from "drizzle-orm/pg-core";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
5
|
+
/**
|
|
6
|
+
* The five read-side members {@link ReadHandle} narrows a handle to, as a
|
|
7
|
+
* constraint.
|
|
8
|
+
*
|
|
9
|
+
* `Pick<T, K>` requires `K extends keyof T`, which an unconstrained type
|
|
10
|
+
* parameter cannot satisfy; this interface is what lets {@link ReadHandle}
|
|
11
|
+
* name the five keys structurally instead. Members are typed `unknown` on
|
|
12
|
+
* purpose — the constraint asserts only that a handle HAS a read surface,
|
|
13
|
+
* never what it looks like, so the driver keeps full control of the
|
|
14
|
+
* signatures a consumer sees.
|
|
15
|
+
*/
|
|
16
|
+
export interface ReadSurface {
|
|
17
|
+
readonly select: unknown;
|
|
18
|
+
readonly selectDistinct: unknown;
|
|
19
|
+
readonly selectDistinctOn: unknown;
|
|
20
|
+
readonly with: unknown;
|
|
21
|
+
readonly $count: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The read surface shared by an Effect-native Drizzle database and its
|
|
25
|
+
* transaction: every way of starting a SELECT, and nothing that writes.
|
|
26
|
+
*
|
|
27
|
+
* A read may run either side of a transaction boundary, so the value
|
|
28
|
+
* {@link DatabaseServices.readHandle} yields has to be the part both handles
|
|
29
|
+
* agree on. Narrowing to it is a `Pick`, never a cast: both handles satisfy
|
|
30
|
+
* it structurally, and the driver's own signatures survive the narrowing.
|
|
31
|
+
*/
|
|
32
|
+
export type ReadHandle<Db extends ReadSurface> = Pick<Db, "select" | "selectDistinct" | "selectDistinctOn" | "with" | "$count">;
|
|
33
|
+
/**
|
|
34
|
+
* The one method {@link DatabaseServices.transact} needs from a handle: open
|
|
35
|
+
* a transaction, run an Effect against it, commit on success and roll back on
|
|
36
|
+
* failure.
|
|
37
|
+
*
|
|
38
|
+
* Both the database and the transaction satisfy this — a transaction's own
|
|
39
|
+
* `transaction` opens a SAVEPOINT — which is what makes nesting expressible
|
|
40
|
+
* without a second type.
|
|
41
|
+
*
|
|
42
|
+
* `config` is optional here and absent from a transaction handle's own
|
|
43
|
+
* signature; a method that ignores it stays assignable, so one interface
|
|
44
|
+
* covers both.
|
|
45
|
+
*/
|
|
46
|
+
export interface TransactionalDatabase<Tx> {
|
|
47
|
+
readonly transaction: <A, E, R>(f: (tx: Tx) => Effect.Effect<A, E, R>, config?: PgTransactionConfig) => Effect.Effect<A, E | SqlError, R>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The services one application's database handles are reached through: the
|
|
51
|
+
* two service keys, the write boundary, and the read-side resolver.
|
|
52
|
+
*
|
|
53
|
+
* The identifier of each key is the **key string itself**, deliberately: an
|
|
54
|
+
* application writes `type Database = typeof services.Database.Identifier`
|
|
55
|
+
* and gets a requirement it can name in its own signatures, the way a
|
|
56
|
+
* class-style `Context.Service` gives the class. Two factories called with
|
|
57
|
+
* different keys therefore produce services that are distinct to the type
|
|
58
|
+
* checker as well as at runtime.
|
|
59
|
+
*/
|
|
60
|
+
export interface DatabaseServices<Db, Tx extends ReadSurface, Key extends string> {
|
|
61
|
+
/**
|
|
62
|
+
* The canonical database service: the Effect-native Drizzle client, keyed
|
|
63
|
+
* by the string the factory was given. Queries yielded from it are Effects
|
|
64
|
+
* executed against whatever pool the layer providing it holds open.
|
|
65
|
+
*/
|
|
66
|
+
readonly Database: Context.Service<Key, Db>;
|
|
67
|
+
/**
|
|
68
|
+
* The ambient transaction handle for the unit of work in flight.
|
|
69
|
+
*
|
|
70
|
+
* Providing it is {@link transact}'s job; an operation that needs the
|
|
71
|
+
* handle yields this key instead of taking a `tx` parameter, so "must run
|
|
72
|
+
* inside a transaction" lands in the `R` channel where the compiler checks
|
|
73
|
+
* it rather than in a parameter a caller can satisfy with the wrong handle.
|
|
74
|
+
*
|
|
75
|
+
* The payload is NOT interchangeable with {@link Database}. A transaction
|
|
76
|
+
* handle is the CONCRETE transaction the driver passed to the callback,
|
|
77
|
+
* whereas the `Database` payload can be a proxy the consuming application
|
|
78
|
+
* wrapped around its client (website #593 depends on that difference for
|
|
79
|
+
* subquery recursion). Read this key as "the open transaction", not as
|
|
80
|
+
* "permission to write".
|
|
81
|
+
*
|
|
82
|
+
* The key string is the `Database` key plus `/CurrentTransaction`, so the
|
|
83
|
+
* pair is legible in a context dump and two applications composed into one
|
|
84
|
+
* process never collide.
|
|
85
|
+
*/
|
|
86
|
+
readonly CurrentTransaction: Context.Service<`${Key}/CurrentTransaction`, Tx>;
|
|
87
|
+
/**
|
|
88
|
+
* Runs `body` inside a database transaction, providing the handle as
|
|
89
|
+
* {@link CurrentTransaction}.
|
|
90
|
+
*
|
|
91
|
+
* Replaces the hand-rolled `db.transaction((tx) => …)` boundary at every
|
|
92
|
+
* call site: the handle is yielded here, the driver's callback parameter is
|
|
93
|
+
* discharged into the context, and `CurrentTransaction` leaves the
|
|
94
|
+
* requirement channel while everything else in it is preserved.
|
|
95
|
+
*
|
|
96
|
+
* **Always-open, never join-if-present.** Calling `transact` while a
|
|
97
|
+
* transaction is already open deliberately opens a NESTED one (a SAVEPOINT
|
|
98
|
+
* on the same connection), exactly as the hand-rolled `db.transaction` it
|
|
99
|
+
* replaces did. A "join the ambient transaction if there is one" variant is
|
|
100
|
+
* easy to write and is the wrong call here: it would silently delete every
|
|
101
|
+
* SAVEPOINT the calling code currently takes, changing rollback granularity
|
|
102
|
+
* without changing a line of that code. If you want to reuse the open
|
|
103
|
+
* transaction, yield {@link CurrentTransaction} directly and do not open
|
|
104
|
+
* another. Adjudicated on website #1883 (owner, 2026-09-07, condition 3) —
|
|
105
|
+
* please do not "improve" this into a join-if-present helper.
|
|
106
|
+
*
|
|
107
|
+
* The nested boundary is opened on the ambient handle
|
|
108
|
+
* (`tx.transaction(…)`), not on the client. Some drivers pin the open
|
|
109
|
+
* connection to the fiber and would nest either way; a driver that does not
|
|
110
|
+
* would hand `db.transaction` a SECOND pooled connection, making the
|
|
111
|
+
* "nested" transaction an independent one that cannot see its parent's
|
|
112
|
+
* uncommitted writes and does not roll back with it. Going through the
|
|
113
|
+
* ambient handle is a SAVEPOINT on every driver.
|
|
114
|
+
*/
|
|
115
|
+
readonly transact: <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError, Exclude<R, `${Key}/CurrentTransaction`> | Key>;
|
|
116
|
+
/**
|
|
117
|
+
* The handle a **read** should use: the ambient {@link CurrentTransaction}
|
|
118
|
+
* when one is open, the {@link Database} client otherwise.
|
|
119
|
+
*
|
|
120
|
+
* Reads sit either side of a boundary. A snapshot reconstruction runs with
|
|
121
|
+
* no transaction in sight and also runs inside a restore's; a
|
|
122
|
+
* carry-forward read happens once on a plain GET and again inside the
|
|
123
|
+
* update. Threading a handle through those is what keeps a
|
|
124
|
+
* `Pick<Tx, "select">` parameter alive after the services have removed
|
|
125
|
+
* every other one — functions carrying a parameter purely because they are
|
|
126
|
+
* honest about being callable from both sides.
|
|
127
|
+
*
|
|
128
|
+
* Preferring the transaction is also the safer default, not merely the
|
|
129
|
+
* convenient one. A read issued against `Database` from inside an open
|
|
130
|
+
* transaction takes its own pooled connection, so it is not guaranteed to
|
|
131
|
+
* see that transaction's uncommitted writes — a carry-forward read that
|
|
132
|
+
* missed the row the same unit of work had just written would be a silent
|
|
133
|
+
* wrong-answer bug. Reading through the open transaction removes the
|
|
134
|
+
* question. (A test harness that pins the pool to one connection cannot
|
|
135
|
+
* demonstrate the difference; the reasoning is about the pooled production
|
|
136
|
+
* path.)
|
|
137
|
+
*
|
|
138
|
+
* This is deliberately **not** what {@link transact} does for writes.
|
|
139
|
+
* There, joining an ambient transaction would delete a SAVEPOINT and change
|
|
140
|
+
* rollback granularity, which is why it is always-open (website #1883,
|
|
141
|
+
* owner, 2026-09-07). A read opens nothing, so there is no boundary to lose
|
|
142
|
+
* — the asymmetry is the point, not an inconsistency.
|
|
143
|
+
*
|
|
144
|
+
* Not for a caller that needs a handle which is concretely NOT the
|
|
145
|
+
* consuming application's proxy (website #593). This falls back to exactly
|
|
146
|
+
* that proxy when no transaction is open, so such a caller must keep
|
|
147
|
+
* opening one of its own.
|
|
148
|
+
*
|
|
149
|
+
* The requirement channel says {@link Database} either way:
|
|
150
|
+
* `Effect.serviceOption` does not surface `CurrentTransaction` in `R`, so
|
|
151
|
+
* the preference is invisible to the type checker and is pinned by a test
|
|
152
|
+
* instead.
|
|
153
|
+
*/
|
|
154
|
+
readonly readHandle: Effect.Effect<ReadHandle<Tx>, never, Key>;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Builds one application's {@link DatabaseServices} around its own database
|
|
158
|
+
* and transaction types.
|
|
159
|
+
*
|
|
160
|
+
* Curried in two stages because TypeScript has no partial type-argument
|
|
161
|
+
* inference: the first call fixes the driver's handle types (which a consumer
|
|
162
|
+
* always writes out — they carry its relations graph), the second infers the
|
|
163
|
+
* key string literal from the argument. Call it once per database, at module
|
|
164
|
+
* scope, and export what it returns; calling it twice with the same key
|
|
165
|
+
* produces services that share a runtime slot.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```ts
|
|
169
|
+
* import { makeDatabaseServices } from "@thomasfosterau/effect-drizzle-ext/database";
|
|
170
|
+
* // Any driver's handle types fit; these are the `@effect/sql-pg` binding's.
|
|
171
|
+
* import type { PgDatabaseClient, PgTransaction } from "@thomasfosterau/effect-drizzle-ext-pg";
|
|
172
|
+
*
|
|
173
|
+
* const relations = {}; // the application's drizzle relations graph
|
|
174
|
+
* type AppDatabase = PgDatabaseClient<typeof relations>;
|
|
175
|
+
* type AppTransaction = PgTransaction<typeof relations>;
|
|
176
|
+
*
|
|
177
|
+
* const services = makeDatabaseServices<AppDatabase, AppTransaction>()({
|
|
178
|
+
* key: "Database",
|
|
179
|
+
* });
|
|
180
|
+
* export const { Database, CurrentTransaction, transact, readHandle } = services;
|
|
181
|
+
* export type Database = typeof services.Database.Identifier; // "Database"
|
|
182
|
+
*
|
|
183
|
+
* console.log(Database.key); // "Database"
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export declare const makeDatabaseServices: <Db extends TransactionalDatabase<Tx> & ReadHandle<Tx>, Tx extends ReadSurface & TransactionalDatabase<Tx>>() => <const Key extends string>(options: {
|
|
187
|
+
readonly key: Key;
|
|
188
|
+
}) => DatabaseServices<Db, Tx, Key>;
|
|
189
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","names":[],"sources":["../src/database.ts"],"mappings":";;;;;;;;;;;;;;;iBAiDiB;WACN;WACA;WACA;WACA;WACA;;;;;;;;;;;YAYC,WAAW,WAAW,eAAe,KAC/C;;;;;;;;;;;;;;iBAiBe,sBAAsB;WAC5B,cAAc,GAAG,GAAG,GAC3B,IAAI,IAAI,OAAO,OAAO,OAAO,GAAG,GAAG,IACnC,SAAS,wBACN,OAAO,OAAO,GAAG,IAAI,UAAU;;;;;;;;;;;;;iBAcrB,iBAAiB,IAAI,WAAW,aAAa;;;;;;WAMnD,UAAU,QAAQ,QAAQ,KAAK;;;;;;;;;;;;;;;;;;;;WAqB/B,oBAAoB,QAAQ,WAAW,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA8BjE,WAAW,GAAG,GAAG,GACxB,MAAM,OAAO,OAAO,GAAG,GAAG,OACvB,OAAO,OAAO,GAAG,IAAI,UAAU,QAAQ,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAwCrE,YAAY,OAAO,OAAO,WAAW,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiC/C,uBAET,WAAW,sBAAsB,MAAM,WAAW,KAClD,WAAW,cAAc,sBAAsB,iBAE1C,oBAAkB;WAAsB,KAAK;MAAQ,iBAAiB,IAAI,IAAI"}
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
// src/database.ts
|
|
5
|
+
/**
|
|
6
|
+
* Builds one application's {@link DatabaseServices} around its own database
|
|
7
|
+
* and transaction types.
|
|
8
|
+
*
|
|
9
|
+
* Curried in two stages because TypeScript has no partial type-argument
|
|
10
|
+
* inference: the first call fixes the driver's handle types (which a consumer
|
|
11
|
+
* always writes out — they carry its relations graph), the second infers the
|
|
12
|
+
* key string literal from the argument. Call it once per database, at module
|
|
13
|
+
* scope, and export what it returns; calling it twice with the same key
|
|
14
|
+
* produces services that share a runtime slot.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { makeDatabaseServices } from "@thomasfosterau/effect-drizzle-ext/database";
|
|
19
|
+
* // Any driver's handle types fit; these are the `@effect/sql-pg` binding's.
|
|
20
|
+
* import type { PgDatabaseClient, PgTransaction } from "@thomasfosterau/effect-drizzle-ext-pg";
|
|
21
|
+
*
|
|
22
|
+
* const relations = {}; // the application's drizzle relations graph
|
|
23
|
+
* type AppDatabase = PgDatabaseClient<typeof relations>;
|
|
24
|
+
* type AppTransaction = PgTransaction<typeof relations>;
|
|
25
|
+
*
|
|
26
|
+
* const services = makeDatabaseServices<AppDatabase, AppTransaction>()({
|
|
27
|
+
* key: "Database",
|
|
28
|
+
* });
|
|
29
|
+
* export const { Database, CurrentTransaction, transact, readHandle } = services;
|
|
30
|
+
* export type Database = typeof services.Database.Identifier; // "Database"
|
|
31
|
+
*
|
|
32
|
+
* console.log(Database.key); // "Database"
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
const makeDatabaseServices = () => (options) => {
|
|
36
|
+
const Database = Context.Service(options.key);
|
|
37
|
+
const CurrentTransaction = Context.Service(`${options.key}/CurrentTransaction`);
|
|
38
|
+
/** Widens either handle to the read surface without a cast — both satisfy it structurally. */
|
|
39
|
+
const asReadHandle = (handle) => handle;
|
|
40
|
+
/** Opens the body's boundary on `handle`, discharging the new transaction into the context. */
|
|
41
|
+
const openOn = (handle, body) => handle.transaction((tx) => Effect.provideService(body, CurrentTransaction, tx));
|
|
42
|
+
return {
|
|
43
|
+
Database,
|
|
44
|
+
CurrentTransaction,
|
|
45
|
+
transact: (body) => Effect.flatMap(Effect.serviceOption(CurrentTransaction), (open) => Option.isSome(open) ? openOn(open.value, body) : Effect.flatMap(Database, (db) => openOn(db, body))),
|
|
46
|
+
readHandle: Effect.flatMap(Effect.serviceOption(CurrentTransaction), (open) => Option.isSome(open) ? Effect.succeed(asReadHandle(open.value)) : Effect.map(Database, asReadHandle))
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export { makeDatabaseServices };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../src/database.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0OA,MAAa,8BAKgB,YAAkE;CAC3F,MAAM,WAAW,QAAQ,QAAiB,QAAQ,GAAG;CACrD,MAAM,qBAAqB,QAAQ,QACjC,GAAG,QAAQ,IAAI,oBACjB;;CAGA,MAAM,gBAAgB,WAA2C;;CAGjE,MAAM,UACJ,QACA,SAEA,OAAO,aAAa,OAAO,OAAO,eAAe,MAAM,oBAAoB,EAAE,CAAC;CAEhF,OAAO;EACL;EACA;EACA,WACE,SAEA,OAAO,QAAQ,OAAO,cAAc,kBAAkB,IAAI,SACxD,OAAO,OAAO,IAAI,IACd,OAAO,KAAK,OAAO,IAAI,IACvB,OAAO,QAAQ,WAAW,OAAO,OAAO,IAAI,IAAI,CAAC,CACvD;EACF,YAAY,OAAO,QAAQ,OAAO,cAAc,kBAAkB,IAAI,SACpE,OAAO,OAAO,IAAI,IACd,OAAO,QAAQ,aAAa,KAAK,KAAK,CAAC,IACvC,OAAO,IAAI,UAAU,YAAY,CACvC;CACF;AACF"}
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { L as DerivationError, i as DbColumn, l as FieldColumnsMetaOf, n as ColumnBounds, o as DbColumnReference } from "./index-CFhgcF4N.js";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { ColumnBuilderBase } from "drizzle-orm";
|
|
4
|
+
import { AnyPgColumn, ExtraConfigColumn, PgTableExtraConfigValue } from "drizzle-orm/pg-core";
|
|
5
|
+
/**
|
|
6
|
+
* A registry mapping column kinds to Drizzle column builders — one builder per
|
|
7
|
+
* kind, taking the column name and returning a fresh (nullable) builder. The
|
|
8
|
+
* consumer supplies this; the package provides no fixed set.
|
|
9
|
+
*/
|
|
10
|
+
export type ColumnBuilders<Kind extends string = string> = Record<Kind, (column: string) => ColumnBuilderBase>;
|
|
11
|
+
/** The (nullable) builder a kind produces in registry `B`. */
|
|
12
|
+
type Builder<B extends ColumnBuilders, P extends keyof B> = ReturnType<B[P]>;
|
|
13
|
+
/** The `.notNull()` form of a builder. */
|
|
14
|
+
type NotNull<X> = X extends {
|
|
15
|
+
notNull(): infer R;
|
|
16
|
+
} ? R : X;
|
|
17
|
+
/**
|
|
18
|
+
* The Drizzle column builder a single db-column annotation produces against
|
|
19
|
+
* registry `B`. Total over any `M` (a widened `unknown` metadata falls back to
|
|
20
|
+
* the base builder type), computing the precise builder type for a concrete
|
|
21
|
+
* `{ pg, notNull }` metadata.
|
|
22
|
+
*
|
|
23
|
+
* Exported so a projection-specific derivation (the `./jsonapi/derive` entry)
|
|
24
|
+
* can name this type without restating it.
|
|
25
|
+
*/
|
|
26
|
+
export type BuilderFor<M, B extends ColumnBuilders> = M extends {
|
|
27
|
+
readonly pg: infer P;
|
|
28
|
+
readonly notNull: infer N;
|
|
29
|
+
} ? P extends keyof B ? N extends true ? NotNull<Builder<B, P>> : Builder<B, P> : ColumnBuilderBase : ColumnBuilderBase;
|
|
30
|
+
/**
|
|
31
|
+
* The Drizzle column builders a db-column metadata map produces against
|
|
32
|
+
* registry `B`, keyed and typed per entry — the type-level counterpart of the
|
|
33
|
+
* builder loop {@link makeDeriveColumns} returns.
|
|
34
|
+
*/
|
|
35
|
+
export type DerivedColumns<Meta, B extends ColumnBuilders> = { readonly [K in keyof Meta]: BuilderFor<Meta[K], B>; };
|
|
36
|
+
/**
|
|
37
|
+
* Optional hooks for {@link makeDeriveColumns}. `resolveReference` turns a
|
|
38
|
+
* column's {@link DbColumnReference} name pair into the Drizzle column thunk
|
|
39
|
+
* `.references()` needs — the seam that lets FK-backed attributes derive their
|
|
40
|
+
* column instead of being hand-authored. Without it, a `references` annotation
|
|
41
|
+
* is a configuration error (throws), so a consumer only wires it when it
|
|
42
|
+
* declares FK columns. The `./references` entry ships one implementation.
|
|
43
|
+
*/
|
|
44
|
+
export interface DeriveOptions {
|
|
45
|
+
readonly resolveReference?: ((reference: DbColumnReference) => () => AnyPgColumn) | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The derivation pair {@link makeDeriveColumns} returns for builder registry
|
|
49
|
+
* `B`: `fromMeta` over an already-projected metadata map, `fromFields` over a
|
|
50
|
+
* `Schema.Struct`'s fields. Both throw {@link DerivationError} at module load
|
|
51
|
+
* on a misdeclared column.
|
|
52
|
+
*/
|
|
53
|
+
export interface DeriveColumns<B extends ColumnBuilders> {
|
|
54
|
+
/** Derive the builders an already-projected metadata map declares. */
|
|
55
|
+
readonly fromMeta: <Meta extends Record<string, DbColumn>>(metas: Meta) => DerivedColumns<Meta, B>;
|
|
56
|
+
/** Derive the builders a `Schema.Struct`'s annotated fields declare. */
|
|
57
|
+
readonly fromFields: <F extends Schema.Struct.Fields>(fields: F) => DerivedColumns<FieldColumnsMetaOf<F>, B>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a derivation bound to a specific builder registry. The returned pair
|
|
61
|
+
* interprets per-column `dbColumn` metadata into the Drizzle column builders a
|
|
62
|
+
* table factory's `columns` expects:
|
|
63
|
+
*
|
|
64
|
+
* - `fromFields` takes a `Schema.Struct`'s fields directly (peeling the
|
|
65
|
+
* `optional` / `NullOr` wrappers via {@link fieldColumns});
|
|
66
|
+
* - `fromMeta` takes an already-projected metadata map, for a projection this
|
|
67
|
+
* entry knows nothing about (the `./jsonapi/derive` entry is one).
|
|
68
|
+
*/
|
|
69
|
+
export declare const makeDeriveColumns: <B extends ColumnBuilders>(builders: B, options?: DeriveOptions) => DeriveColumns<B>;
|
|
70
|
+
/**
|
|
71
|
+
* Derive the SQL `CHECK` constraints an already-projected bounds map implies —
|
|
72
|
+
* the third leg of annotation-driven columns, after the column itself and the
|
|
73
|
+
* read-view selection. Emits one Drizzle `check()` per bounded column, named
|
|
74
|
+
* `${tableName}_${column}_${suffix}` (see {@link boundSuffix} for the canonical
|
|
75
|
+
* suffixes) unless the column annotation pins a `checkName`. A nullable
|
|
76
|
+
* column's expression is guarded with `col IS NULL OR …`, matching the
|
|
77
|
+
* hand-authored precedents.
|
|
78
|
+
*
|
|
79
|
+
* Returns a callback shaped for a table factory's `extraConstraints` seam: it
|
|
80
|
+
* receives the table's columns keyed by **attribute** name (not SQL column
|
|
81
|
+
* name). A bounded attribute whose column is absent from the table is a
|
|
82
|
+
* configuration error ({@link DerivationError}) — bounds on a column the table
|
|
83
|
+
* doesn't carry cannot be enforced.
|
|
84
|
+
*/
|
|
85
|
+
export declare const deriveCheckConstraintsFrom: (bounds: Record<string, ColumnBounds>, tableName: string) => (table: Record<string, ExtraConfigColumn>) => PgTableExtraConfigValue[];
|
|
86
|
+
/**
|
|
87
|
+
* {@link deriveCheckConstraintsFrom} over a `Schema.Struct`'s fields: the
|
|
88
|
+
* bounds a model's `Schema.isGreaterThanOrEqualTo` / `isBetween` checks declare
|
|
89
|
+
* ({@link fieldColumnBounds}) become the table's CHECK constraints. One
|
|
90
|
+
* declaration drives wire validation and the database constraint.
|
|
91
|
+
*
|
|
92
|
+
* The projection is deferred to the returned callback, so this composes inside
|
|
93
|
+
* a `pgTable(...)` call whose fields are still being assembled.
|
|
94
|
+
*/
|
|
95
|
+
export declare const deriveFieldCheckConstraints: (fields: Schema.Struct.Fields, tableName: string) => (table: Record<string, ExtraConfigColumn>) => PgTableExtraConfigValue[];
|
|
96
|
+
export { DerivationError };
|
|
97
|
+
//# sourceMappingURL=derive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.d.ts","names":[],"sources":["../src/derive.ts"],"mappings":";;;;;;;;;YAmEY,eAAe,gCAAgC,OACzD,OACC,mBAAmB;;KAIjB,QAAQ,UAAU,gBAAgB,gBAAgB,KAAK,WAAW,EAAE;;KAGpE,QAAQ,KAAK;EAAY,iBAAiB;IAAM,IAAI;;;;;;;;;;YAW7C,WAAW,GAAG,UAAU,kBAAkB;WAC3C,UAAU;WACV,eAAe;IAEtB,gBAAgB,IACd,iBACE,QAAQ,QAAQ,GAAG,MACnB,QAAQ,GAAG,KACb,oBACF;;;;;;YAOQ,eAAe,MAAM,UAAU,8BAC/B,WAAW,OAAO,WAAW,KAAK,IAAI;;;;;;;;;iBA0BjC;WACN,qBAAqB,WAAW,4BAA4B;;;;;;;;iBA0BtD,cAAc,UAAU;;WAE9B,WAAW,aAAa,eAAe,WAC9C,OAAO,SACJ,eAAe,MAAM;;WAEjB,aAAa,UAAU,OAAO,OAAO,QAC5C,QAAQ,MACL,eAAe,mBAAmB,IAAI;;;;;;;;;;;;qBAahC,oBAAqB,UAAU,gBAAc,UAC9C,GAAC,UACF,kBACR,cAAc;;;;;;;;;;;;;;;;qBAwGJ,6BAA0B,QAC5B,eAAe,eAAa,uBAAmB,OAChD,eAAe,uBAAqB;;;;;;;;;;qBA6BjC,8BAA2B,QAC7B,OAAO,OAAO,QAAM,uBAAmB,OACxC,eAAe,uBAAqB"}
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { c as fieldColumnBounds, h as DerivationError, l as fieldColumns } from "./src-CBeuhy5b.js";
|
|
2
|
+
import * as Arr from "effect/Array";
|
|
3
|
+
import * as R from "effect/Record";
|
|
4
|
+
import "effect/Schema";
|
|
5
|
+
import { sql } from "drizzle-orm";
|
|
6
|
+
import { check } from "drizzle-orm/pg-core";
|
|
7
|
+
// src/derive.ts
|
|
8
|
+
/**
|
|
9
|
+
* @copyright Thomas Foster
|
|
10
|
+
*
|
|
11
|
+
* **Effect Schema → Drizzle derivation: the derivation entry (Drizzle).**
|
|
12
|
+
*
|
|
13
|
+
* Reads the {@link DbColumn} metadata a schema declaration carries (via
|
|
14
|
+
* {@link fieldColumns} from the {@link ./index.ts} annotation entry, or via any
|
|
15
|
+
* other projection that yields a `Record<string, DbColumn>`) and emits the
|
|
16
|
+
* matching Drizzle column builders. Depends on Drizzle, so a model / client
|
|
17
|
+
* layer importing only the annotation entry never pulls the ORM.
|
|
18
|
+
*
|
|
19
|
+
* **Extensible column kinds.** The kind → builder mapping is *injected*, not
|
|
20
|
+
* baked in: a consumer passes a builder registry (a record from its own kind
|
|
21
|
+
* union to a Drizzle column builder) to {@link makeDeriveColumns}, and the
|
|
22
|
+
* derivation preserves each column's precise builder type — so the derived
|
|
23
|
+
* column map keeps `$inferSelect` typed — by reading the type-level phantom
|
|
24
|
+
* `dbColumn` stamps alongside the runtime annotation.
|
|
25
|
+
*
|
|
26
|
+
* **Two entry shapes, one interpreter.** {@link makeDeriveColumns} returns both
|
|
27
|
+
* `fromFields` (a `Schema.Struct`'s fields, the generic source) and `fromMeta`
|
|
28
|
+
* (an already-projected metadata map, the seam the `./jsonapi/derive` entry and
|
|
29
|
+
* any other projection build on). They share one builder loop.
|
|
30
|
+
*
|
|
31
|
+
* **Failures here are THROWN, not Effect failures.** Everything in this module
|
|
32
|
+
* runs at *module load*, while a consumer's table modules are being evaluated —
|
|
33
|
+
* before any Effect runtime exists to run a failure in, and before any query
|
|
34
|
+
* can be issued. A misdeclared table must fail at `import`, not at first
|
|
35
|
+
* insert, so a missing builder / a missing reference hook / bounds on a column
|
|
36
|
+
* the table does not carry throw {@link DerivationError}. Runtime query
|
|
37
|
+
* failures are the opposite and are never thrown: they travel in the Effect
|
|
38
|
+
* error channel as the tagged errors of the `./errors` entry.
|
|
39
|
+
*
|
|
40
|
+
* @module derive
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Build a derivation bound to a specific builder registry. The returned pair
|
|
44
|
+
* interprets per-column `dbColumn` metadata into the Drizzle column builders a
|
|
45
|
+
* table factory's `columns` expects:
|
|
46
|
+
*
|
|
47
|
+
* - `fromFields` takes a `Schema.Struct`'s fields directly (peeling the
|
|
48
|
+
* `optional` / `NullOr` wrappers via {@link fieldColumns});
|
|
49
|
+
* - `fromMeta` takes an already-projected metadata map, for a projection this
|
|
50
|
+
* entry knows nothing about (the `./jsonapi/derive` entry is one).
|
|
51
|
+
*/
|
|
52
|
+
const makeDeriveColumns = (builders, options = {}) => {
|
|
53
|
+
const buildColumn = (meta) => {
|
|
54
|
+
const build = builders[meta.pg];
|
|
55
|
+
if (build === void 0) throw new DerivationError({ message: `no builder registered for column kind '${meta.pg}'` });
|
|
56
|
+
let column = build(meta.column);
|
|
57
|
+
if (meta.notNull) column = column.notNull();
|
|
58
|
+
if (meta.default !== void 0) column = column.default(meta.default);
|
|
59
|
+
if (meta.references !== void 0) {
|
|
60
|
+
if (options.resolveReference === void 0) throw new DerivationError({ message: `column '${meta.column}' declares a reference but no resolveReference hook was provided` });
|
|
61
|
+
const target = options.resolveReference(meta.references);
|
|
62
|
+
column = column.references(target, {
|
|
63
|
+
onDelete: meta.references.onDelete,
|
|
64
|
+
onUpdate: meta.references.onUpdate
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return column;
|
|
68
|
+
};
|
|
69
|
+
const buildColumns = (metas) => R.fromEntries(Arr.map(R.toEntries(metas), ([name, meta]) => [name, buildColumn(meta)]));
|
|
70
|
+
return {
|
|
71
|
+
fromMeta: (metas) => buildColumns(metas),
|
|
72
|
+
fromFields: (fields) => buildColumns(fieldColumns(fields))
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** A number rendered inline into the CHECK expression (never a bind parameter). */
|
|
76
|
+
const literal = (value) => sql.raw(String(value));
|
|
77
|
+
/**
|
|
78
|
+
* The canonical constraint-name suffix a set of bounds earns: `range` when
|
|
79
|
+
* bounded on both sides; `non_negative` for the sole inclusive-minimum-of-`0`
|
|
80
|
+
* case (the established naming for count/duration columns); `min` / `max` for
|
|
81
|
+
* the remaining one-sided bounds.
|
|
82
|
+
*/
|
|
83
|
+
const boundSuffix = (bounds) => {
|
|
84
|
+
const hasMin = bounds.minimum !== void 0 || bounds.exclusiveMinimum !== void 0;
|
|
85
|
+
const hasMax = bounds.maximum !== void 0 || bounds.exclusiveMaximum !== void 0;
|
|
86
|
+
if (hasMin && hasMax) return "range";
|
|
87
|
+
if (!hasMin) return "max";
|
|
88
|
+
return bounds.minimum === 0 ? "non_negative" : "min";
|
|
89
|
+
};
|
|
90
|
+
/** The per-side comparisons for a bound set (exclusive bounds win their side). */
|
|
91
|
+
const comparisonParts = (column, bounds) => {
|
|
92
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = bounds;
|
|
93
|
+
const parts = [];
|
|
94
|
+
if (exclusiveMinimum !== void 0) parts.push(sql`${column} > ${literal(exclusiveMinimum)}`);
|
|
95
|
+
else if (minimum !== void 0) parts.push(sql`${column} >= ${literal(minimum)}`);
|
|
96
|
+
if (exclusiveMaximum !== void 0) parts.push(sql`${column} < ${literal(exclusiveMaximum)}`);
|
|
97
|
+
else if (maximum !== void 0) parts.push(sql`${column} <= ${literal(maximum)}`);
|
|
98
|
+
return parts;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The bound expression for one column (without the nullability guard): a
|
|
102
|
+
* both-sides-inclusive bound renders as `col BETWEEN min AND max` — matching
|
|
103
|
+
* the hand-authored year-range constraints in the consuming applications — and
|
|
104
|
+
* anything else as the {@link comparisonParts} joined with `AND`.
|
|
105
|
+
*/
|
|
106
|
+
const boundExpression = (column, bounds) => bounds.minimum !== void 0 && bounds.maximum !== void 0 ? sql`${column} BETWEEN ${literal(bounds.minimum)} AND ${literal(bounds.maximum)}` : sql.join(comparisonParts(column, bounds), sql` AND `);
|
|
107
|
+
/**
|
|
108
|
+
* Derive the SQL `CHECK` constraints an already-projected bounds map implies —
|
|
109
|
+
* the third leg of annotation-driven columns, after the column itself and the
|
|
110
|
+
* read-view selection. Emits one Drizzle `check()` per bounded column, named
|
|
111
|
+
* `${tableName}_${column}_${suffix}` (see {@link boundSuffix} for the canonical
|
|
112
|
+
* suffixes) unless the column annotation pins a `checkName`. A nullable
|
|
113
|
+
* column's expression is guarded with `col IS NULL OR …`, matching the
|
|
114
|
+
* hand-authored precedents.
|
|
115
|
+
*
|
|
116
|
+
* Returns a callback shaped for a table factory's `extraConstraints` seam: it
|
|
117
|
+
* receives the table's columns keyed by **attribute** name (not SQL column
|
|
118
|
+
* name). A bounded attribute whose column is absent from the table is a
|
|
119
|
+
* configuration error ({@link DerivationError}) — bounds on a column the table
|
|
120
|
+
* doesn't carry cannot be enforced.
|
|
121
|
+
*/
|
|
122
|
+
const deriveCheckConstraintsFrom = (bounds, tableName) => (table) => Arr.map(R.toEntries(bounds), ([attribute, columnBounds]) => {
|
|
123
|
+
const column = table[attribute];
|
|
124
|
+
if (column === void 0) throw new DerivationError({ message: `attribute '${attribute}' declares numeric bounds but table '${tableName}' has no column for it` });
|
|
125
|
+
const expression = boundExpression(column, columnBounds);
|
|
126
|
+
const guarded = columnBounds.column.notNull ? expression : sql`${column} IS NULL OR ${expression}`;
|
|
127
|
+
const name = columnBounds.column.checkName ?? `${tableName}_${columnBounds.column.column}_${boundSuffix(columnBounds)}`;
|
|
128
|
+
return check(name, guarded);
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* {@link deriveCheckConstraintsFrom} over a `Schema.Struct`'s fields: the
|
|
132
|
+
* bounds a model's `Schema.isGreaterThanOrEqualTo` / `isBetween` checks declare
|
|
133
|
+
* ({@link fieldColumnBounds}) become the table's CHECK constraints. One
|
|
134
|
+
* declaration drives wire validation and the database constraint.
|
|
135
|
+
*
|
|
136
|
+
* The projection is deferred to the returned callback, so this composes inside
|
|
137
|
+
* a `pgTable(...)` call whose fields are still being assembled.
|
|
138
|
+
*/
|
|
139
|
+
const deriveFieldCheckConstraints = (fields, tableName) => (table) => deriveCheckConstraintsFrom(fieldColumnBounds(fields), tableName)(table);
|
|
140
|
+
export { DerivationError, deriveCheckConstraintsFrom, deriveFieldCheckConstraints, makeDeriveColumns };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=derive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.js","names":[],"sources":["../src/derive.ts"],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
/**
|
|
5
|
+
* The fields of a Postgres error response this library reads, as recovered from
|
|
6
|
+
* wherever the driver's error sits in an error graph. `code` is the SQLSTATE and
|
|
7
|
+
* is always present (finding it is what identifies the node); every other
|
|
8
|
+
* fact is `Option` because Postgres populates it per error class — a `23502`
|
|
9
|
+
* carries `column` and `table` but no `constraint`, a `23503` carries
|
|
10
|
+
* `constraint`, `table` and `detail`.
|
|
11
|
+
*/
|
|
12
|
+
export interface PgErrorFacts {
|
|
13
|
+
/** The five-character SQLSTATE, e.g. `23505`. */
|
|
14
|
+
readonly code: string;
|
|
15
|
+
/** The driver's message, or `""` when the node carries none. */
|
|
16
|
+
readonly message: string;
|
|
17
|
+
/** The violated constraint's name, for the constraint classes that name one. */
|
|
18
|
+
readonly constraint: Option.Option<string>;
|
|
19
|
+
/** The table the statement touched. */
|
|
20
|
+
readonly table: Option.Option<string>;
|
|
21
|
+
/** The column at fault, populated for NOT NULL violations. */
|
|
22
|
+
readonly column: Option.Option<string>;
|
|
23
|
+
/** Postgres' `DETAIL` line, which names the conflicting key values. */
|
|
24
|
+
readonly detail: Option.Option<string>;
|
|
25
|
+
/** The schema the table lives in. */
|
|
26
|
+
readonly schema: Option.Option<string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The driver's Postgres error facts, recovered from anywhere in `error`'s graph:
|
|
30
|
+
* a bare Postgres error, a bare `SqlError` (whose `reason` holds the driver
|
|
31
|
+
* error as its `cause`), or drizzle's `EffectDrizzleQueryError` wrapping an
|
|
32
|
+
* Effect `Cause` that holds the `SqlError`. `Option.none()` when the graph
|
|
33
|
+
* holds no node declaring a SQLSTATE, including for a non-object input.
|
|
34
|
+
*
|
|
35
|
+
* The walk is bounded (depth 8) and cycle-guarded, so a self-referential or
|
|
36
|
+
* pathologically deep graph terminates.
|
|
37
|
+
*/
|
|
38
|
+
export declare const findPgError: (error: unknown) => Option.Option<PgErrorFacts>;
|
|
39
|
+
declare const UniqueViolation_base: Schema.Class<UniqueViolation, Schema.TaggedStruct<"UniqueViolation", {
|
|
40
|
+
readonly constraint: Schema.Option<Schema.String>;
|
|
41
|
+
readonly table: Schema.Option<Schema.String>;
|
|
42
|
+
readonly detail: Schema.Option<Schema.String>;
|
|
43
|
+
readonly cause: Schema.Defect;
|
|
44
|
+
}>, import("effect/Cause").YieldableError>;
|
|
45
|
+
/**
|
|
46
|
+
* SQLSTATE `23505`: a unique or primary key constraint was violated.
|
|
47
|
+
* `cause` is the value classification started from, so the wrapping graph
|
|
48
|
+
* (drizzle's query text and params, the `SqlError`) survives the recovery.
|
|
49
|
+
*/
|
|
50
|
+
export declare class UniqueViolation extends UniqueViolation_base {}
|
|
51
|
+
declare const ForeignKeyViolation_base: Schema.Class<ForeignKeyViolation, Schema.TaggedStruct<"ForeignKeyViolation", {
|
|
52
|
+
readonly constraint: Schema.Option<Schema.String>;
|
|
53
|
+
readonly table: Schema.Option<Schema.String>;
|
|
54
|
+
readonly detail: Schema.Option<Schema.String>;
|
|
55
|
+
readonly cause: Schema.Defect;
|
|
56
|
+
}>, import("effect/Cause").YieldableError>;
|
|
57
|
+
/** SQLSTATE `23503`: a foreign key constraint was violated. `cause` as in {@link UniqueViolation}. */
|
|
58
|
+
export declare class ForeignKeyViolation extends ForeignKeyViolation_base {}
|
|
59
|
+
declare const NotNullViolation_base: Schema.Class<NotNullViolation, Schema.TaggedStruct<"NotNullViolation", {
|
|
60
|
+
readonly column: Schema.Option<Schema.String>;
|
|
61
|
+
readonly table: Schema.Option<Schema.String>;
|
|
62
|
+
readonly cause: Schema.Defect;
|
|
63
|
+
}>, import("effect/Cause").YieldableError>;
|
|
64
|
+
/** SQLSTATE `23502`: a NOT NULL constraint was violated. `column` names the offending column. */
|
|
65
|
+
export declare class NotNullViolation extends NotNullViolation_base {}
|
|
66
|
+
declare const CheckViolation_base: Schema.Class<CheckViolation, Schema.TaggedStruct<"CheckViolation", {
|
|
67
|
+
readonly constraint: Schema.Option<Schema.String>;
|
|
68
|
+
readonly table: Schema.Option<Schema.String>;
|
|
69
|
+
readonly cause: Schema.Defect;
|
|
70
|
+
}>, import("effect/Cause").YieldableError>;
|
|
71
|
+
/** SQLSTATE `23514`: a CHECK constraint was violated — including the bounds checks `./derive` writes. */
|
|
72
|
+
export declare class CheckViolation extends CheckViolation_base {}
|
|
73
|
+
declare const ExclusionViolation_base: Schema.Class<ExclusionViolation, Schema.TaggedStruct<"ExclusionViolation", {
|
|
74
|
+
readonly constraint: Schema.Option<Schema.String>;
|
|
75
|
+
readonly table: Schema.Option<Schema.String>;
|
|
76
|
+
readonly cause: Schema.Defect;
|
|
77
|
+
}>, import("effect/Cause").YieldableError>;
|
|
78
|
+
/** SQLSTATE `23P01`: an EXCLUDE constraint was violated. */
|
|
79
|
+
export declare class ExclusionViolation extends ExclusionViolation_base {}
|
|
80
|
+
declare const SerializationFailure_base: Schema.Class<SerializationFailure, Schema.TaggedStruct<"SerializationFailure", {
|
|
81
|
+
readonly cause: Schema.Defect;
|
|
82
|
+
}>, import("effect/Cause").YieldableError>;
|
|
83
|
+
/**
|
|
84
|
+
* SQLSTATE `40001`: the transaction lost a serialization conflict. Retryable:
|
|
85
|
+
* the same transaction re-run from the top may commit.
|
|
86
|
+
*/
|
|
87
|
+
export declare class SerializationFailure extends SerializationFailure_base {}
|
|
88
|
+
declare const DeadlockDetected_base: Schema.Class<DeadlockDetected, Schema.TaggedStruct<"DeadlockDetected", {
|
|
89
|
+
readonly cause: Schema.Defect;
|
|
90
|
+
}>, import("effect/Cause").YieldableError>;
|
|
91
|
+
/**
|
|
92
|
+
* SQLSTATE `40P01`: the transaction was chosen as a deadlock victim.
|
|
93
|
+
* Retryable, like {@link SerializationFailure}.
|
|
94
|
+
*/
|
|
95
|
+
export declare class DeadlockDetected extends DeadlockDetected_base {}
|
|
96
|
+
/**
|
|
97
|
+
* The integrity-constraint family (`23xxx`): a failure the *statement* caused,
|
|
98
|
+
* which a caller can map to a domain conflict. Retrying it unchanged fails
|
|
99
|
+
* again.
|
|
100
|
+
*/
|
|
101
|
+
export type ConstraintViolation = UniqueViolation | ForeignKeyViolation | NotNullViolation | CheckViolation | ExclusionViolation;
|
|
102
|
+
/**
|
|
103
|
+
* Every failure this module recognises: a {@link ConstraintViolation}, or one
|
|
104
|
+
* of the two retryable transaction failures.
|
|
105
|
+
*/
|
|
106
|
+
export type ClassifiedError = ConstraintViolation | SerializationFailure | DeadlockDetected;
|
|
107
|
+
/**
|
|
108
|
+
* Classifies any value into one of the seven tagged errors, by finding the
|
|
109
|
+
* driver's Postgres error in its graph ({@link findPgError}) and dispatching on the
|
|
110
|
+
* SQLSTATE. `Option.none()` when the graph holds no driver error, or holds one
|
|
111
|
+
* whose SQLSTATE is outside the classified families (a syntax error, a
|
|
112
|
+
* connection failure — `SqlError.reason` already covers those well).
|
|
113
|
+
*/
|
|
114
|
+
export declare const classifyError: (error: unknown) => Option.Option<ClassifiedError>;
|
|
115
|
+
/**
|
|
116
|
+
* True when `error` is (or wraps) a unique-constraint violation.
|
|
117
|
+
*
|
|
118
|
+
* Kept as its own predicate, with its own walk, for adoption parity: this is
|
|
119
|
+
* the name and the semantics the consuming applications already call, and the
|
|
120
|
+
* semantics include a fallback the typed classification deliberately does not
|
|
121
|
+
* have — matching the stable Postgres message fragment when no `code` survived the
|
|
122
|
+
* wrapping. It also matches a `23505` declared under a `code` that is not
|
|
123
|
+
* SQLSTATE-shaped, which {@link classifyError} skips.
|
|
124
|
+
*/
|
|
125
|
+
export declare const isUniqueViolation: (error: unknown) => boolean;
|
|
126
|
+
/**
|
|
127
|
+
* True when `error` is (or wraps) an integrity-constraint violation — one of
|
|
128
|
+
* the five `23xxx` classes {@link classifyError} recognises.
|
|
129
|
+
*/
|
|
130
|
+
export declare const isConstraintViolation: (error: unknown) => boolean;
|
|
131
|
+
/**
|
|
132
|
+
* True when `error` is (or wraps) a transaction failure that re-running the
|
|
133
|
+
* whole transaction may clear: a serialization failure (`40001`) or a deadlock
|
|
134
|
+
* (`40P01`). Retrying a {@link ConstraintViolation} is pointless; retrying
|
|
135
|
+
* these is the documented remedy.
|
|
136
|
+
*/
|
|
137
|
+
export declare const isRetryableTransactionFailure: (error: unknown) => boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Recovers from a constraint violation in the error channel, leaving every
|
|
140
|
+
* other failure untouched — the failure is re-failed unchanged, with its
|
|
141
|
+
* original type, so a handler for one Postgres conflict does not swallow the
|
|
142
|
+
* rest of the channel.
|
|
143
|
+
*
|
|
144
|
+
* Data-last only: `self.pipe(catchConstraintViolation(handler))`.
|
|
145
|
+
*/
|
|
146
|
+
export declare const catchConstraintViolation: <A2, E2, R2>(f: (violation: ConstraintViolation) => Effect.Effect<A2, E2, R2>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | A2, E | E2, R | R2>;
|
|
147
|
+
/**
|
|
148
|
+
* {@link catchConstraintViolation} narrowed to `23505`: recovers only a unique
|
|
149
|
+
* violation, passing everything else — including other constraint violations —
|
|
150
|
+
* through unchanged. This is the duplicate-key conflict a consumer maps to a
|
|
151
|
+
* domain "already exists".
|
|
152
|
+
*
|
|
153
|
+
* Data-last only: `self.pipe(catchUniqueViolation(handler))`.
|
|
154
|
+
*/
|
|
155
|
+
export declare const catchUniqueViolation: <A2, E2, R2>(f: (violation: UniqueViolation) => Effect.Effect<A2, E2, R2>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | A2, E | E2, R | R2>;
|
|
156
|
+
export {};
|
|
157
|
+
//# sourceMappingURL=errors.d.ts.map
|