@thomasfosterau/effect-drizzle-ext 0.0.0 → 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 +102 -4
- 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,113 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { ConvertCustomConfig, PgCustomColumnBuilder, PgTimestampBuilder, SetHasDefault, SetNotNull } from "drizzle-orm/pg-core";
|
|
3
|
+
/**
|
|
4
|
+
* Builds a typed JSONB Drizzle custom type backed by an Effect Schema.
|
|
5
|
+
*
|
|
6
|
+
* Values are stored as PostgreSQL `jsonb`. The schema validates on both
|
|
7
|
+
* read (`fromDriver`) and write (`toDriver`): writing encodes through the
|
|
8
|
+
* schema and then through {@link JsonText}; reading decodes unknown driver
|
|
9
|
+
* output straight through the schema.
|
|
10
|
+
*
|
|
11
|
+
* The driver this library ships a Postgres binding for (node-pg, via
|
|
12
|
+
* `@effect/sql-pg`) already parses `jsonb` columns before `fromDriver` sees
|
|
13
|
+
* them, so the `typeof value === "string"` branch below is dead under that
|
|
14
|
+
* driver — it exists for a driver that hands back raw JSON text instead,
|
|
15
|
+
* and is exercised directly by this module's tests.
|
|
16
|
+
*
|
|
17
|
+
* Nullable columns are handled transparently — `null` values pass through
|
|
18
|
+
* Drizzle without reaching this codec, so `.nullable()` is not required on
|
|
19
|
+
* `schema`.
|
|
20
|
+
*
|
|
21
|
+
* `schema` is bound to `Schema.Codec<unknown, unknown, never, never>` rather
|
|
22
|
+
* than the wider `Schema.Top`: `toDriver` / `fromDriver` are synchronous
|
|
23
|
+
* Drizzle hooks with no Effect context to satisfy a requirement from, so a
|
|
24
|
+
* schema this codec accepts must already need no services to decode or
|
|
25
|
+
* encode.
|
|
26
|
+
*/
|
|
27
|
+
export declare const jsonbSchema: <S extends Schema.Codec<unknown, unknown, never, never>>(schema: S) => (name: string) => PgCustomColumnBuilder<ConvertCustomConfig<{
|
|
28
|
+
data: S["Type"];
|
|
29
|
+
driverData: unknown;
|
|
30
|
+
}>>;
|
|
31
|
+
/**
|
|
32
|
+
* Custom column type for PostgreSQL `tsvector`.
|
|
33
|
+
*
|
|
34
|
+
* Used for generated full-text search columns. The value is opaque to the
|
|
35
|
+
* consuming application — queries use `sql` template literals for matching.
|
|
36
|
+
*/
|
|
37
|
+
export declare const tsvector: (name: string) => PgCustomColumnBuilder<ConvertCustomConfig<{
|
|
38
|
+
data: string;
|
|
39
|
+
driverData: string;
|
|
40
|
+
}>>;
|
|
41
|
+
/**
|
|
42
|
+
* Custom column type for PostgreSQL `bytea` (binary data).
|
|
43
|
+
*
|
|
44
|
+
* Typed `Uint8Array` rather than `Buffer` — see {@link byteaToHex} for why
|
|
45
|
+
* the Node global is off-limits here. Both directions are honest about the
|
|
46
|
+
* runtime: `@effect/sql-pg`'s binary `bytea` codec decodes to a `Uint8Array`
|
|
47
|
+
* and encodes any `Uint8Array` (a Node `Buffer` included, as a subclass) as
|
|
48
|
+
* raw bytes, so a plain `Uint8Array` binds identically to a `Buffer`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const bytea: (name: string) => PgCustomColumnBuilder<ConvertCustomConfig<{
|
|
51
|
+
data: Uint8Array;
|
|
52
|
+
driverData: Uint8Array;
|
|
53
|
+
}>>;
|
|
54
|
+
/**
|
|
55
|
+
* The `created_at` / `updated_at` builder pair {@link timestamps} returns:
|
|
56
|
+
* `timestamptz`, `NOT NULL`, defaulting to `now()`.
|
|
57
|
+
*/
|
|
58
|
+
export interface TimestampColumns {
|
|
59
|
+
/** `created_at timestamptz NOT NULL DEFAULT now()`. */
|
|
60
|
+
readonly createdAt: SetHasDefault<SetNotNull<PgTimestampBuilder>>;
|
|
61
|
+
/** `updated_at timestamptz NOT NULL DEFAULT now()`. */
|
|
62
|
+
readonly updatedAt: SetHasDefault<SetNotNull<PgTimestampBuilder>>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A fresh `created_at` / `updated_at` pair: `timestamptz`, `NOT NULL`,
|
|
66
|
+
* defaulting to `now()`.
|
|
67
|
+
*
|
|
68
|
+
* A factory, never a shared object: Drizzle column builders are
|
|
69
|
+
* **mutable** — `.notNull()` and `.defaultNow()` mutate and return the same
|
|
70
|
+
* builder instance. A module-level constant reused across tables would
|
|
71
|
+
* therefore hand every table the same two builders, and whichever table
|
|
72
|
+
* was declared last would win any later refinement. Calling `timestamps()`
|
|
73
|
+
* per table mints a fresh pair.
|
|
74
|
+
*
|
|
75
|
+
* `updated_at` defaults rather than auto-updates — nothing in Postgres
|
|
76
|
+
* bumps it, the write path does.
|
|
77
|
+
*
|
|
78
|
+
* Spread rather than a base table: Drizzle records a table's columns in
|
|
79
|
+
* declaration order, and that order is what a consumer's `db:generate`
|
|
80
|
+
* emits as the `CREATE TABLE` column order. Spreading at the position two
|
|
81
|
+
* hand-written lines occupied keeps generated SQL byte-identical, so
|
|
82
|
+
* adopting this helper does not move a baseline migration.
|
|
83
|
+
*/
|
|
84
|
+
export declare const timestamps: () => TimestampColumns;
|
|
85
|
+
/**
|
|
86
|
+
* Renders a `bytea` value (typically a content hash) as its lowercase hex
|
|
87
|
+
* string.
|
|
88
|
+
*
|
|
89
|
+
* Written against `Uint8Array` rather than `Buffer.prototype.toString("hex")`
|
|
90
|
+
* on purpose: a runtime that declares the Node `Buffer` global alongside
|
|
91
|
+
* `@types/node` (Cloudflare Workers' `nodejs_compat` types are one example)
|
|
92
|
+
* can strip the Node-only methods off the global `Buffer` interface, or
|
|
93
|
+
* degrade the `Buffer` *value* to `any`, so every `Buffer.*` call goes
|
|
94
|
+
* unchecked with no signal at all. `Uint8Array` is what a `bytea` row value
|
|
95
|
+
* structurally is on every runtime, so this encodes identically everywhere
|
|
96
|
+
* without depending on that global.
|
|
97
|
+
*/
|
|
98
|
+
export declare const byteaToHex: (bytes: Uint8Array) => string;
|
|
99
|
+
/**
|
|
100
|
+
* Decodes a hex string into the `Uint8Array` a `bytea` column binds — the
|
|
101
|
+
* inverse of {@link byteaToHex}, replacing `Buffer.from(hex, "hex")`.
|
|
102
|
+
*
|
|
103
|
+
* Strict where `Buffer.from` is silent: `Buffer.from` decodes the leading
|
|
104
|
+
* valid pairs and drops the rest, so a malformed hash quietly becomes a
|
|
105
|
+
* short-but-plausible key that matches no row. This throws instead — a
|
|
106
|
+
* caller that takes a hash from outside the trust boundary is expected to
|
|
107
|
+
* validate its shape before calling this, and gets a loud failure instead
|
|
108
|
+
* of a silently truncated key when it doesn't.
|
|
109
|
+
*
|
|
110
|
+
* @throws TypeError if `hex` is not an even-length run of hex digits.
|
|
111
|
+
*/
|
|
112
|
+
export declare const hexToBytea: (hex: string) => Uint8Array;
|
|
113
|
+
//# sourceMappingURL=columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"columns.d.ts","names":[],"sources":["../src/columns.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;qBAwDa,cACV,UAAU,OAAO,uCAAqC,QAAU,OAAC,iBAG/D,sBAAsB;EAAsB,MAAM;EAAW;;;;;;;;qBA0BrD,WAAQ,iBAElB,sBAAsB;EAAsB;EAAc;;;;;;;;;;;qBAgBhD,QAAK,iBAEf,sBAAsB;EAAsB,MAAM;EAAY,YAAY;;;;;;iBAW5D;;WAEN,WAAW,cAAc,WAAW;;WAEpC,WAAW,cAAc,WAAW;;;;;;;;;;;;;;;;;;;;;;qBAuBlC,kBAAiB;;;;;;;;;;;;;;qBAkBjB,aAAU,OAAW;;;;;;;;;;;;;;qBAgBrB,aAAU,gBAAkB"}
|
package/dist/columns.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Predicate from "effect/Predicate";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { customType, timestamp } from "drizzle-orm/pg-core";
|
|
5
|
+
// src/columns.ts
|
|
6
|
+
/**
|
|
7
|
+
* @copyright Thomas Foster
|
|
8
|
+
*
|
|
9
|
+
* Column helpers for Drizzle table definitions this library owns outright:
|
|
10
|
+
* a generic JSONB codec bridging any Effect Schema to `jsonb`, `tsvector`
|
|
11
|
+
* and `bytea` custom types, the `created_at` / `updated_at` timestamp pair
|
|
12
|
+
* every plain table carries, and the hex codec for rendering / parsing
|
|
13
|
+
* `bytea` values without the Node `Buffer` global.
|
|
14
|
+
*
|
|
15
|
+
* @module columns
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The JSON text boundary, expressed as a schema rather than the `JSON`
|
|
19
|
+
* global: encoding runs `JSON.stringify` and decoding runs a total
|
|
20
|
+
* `JSON.parse` whose `None` covers a malformed string.
|
|
21
|
+
*/
|
|
22
|
+
const JsonText = Schema.fromJsonString(Schema.Unknown);
|
|
23
|
+
const encodeJsonText = Schema.encodeSync(JsonText);
|
|
24
|
+
const decodeJsonText = Schema.decodeUnknownOption(JsonText);
|
|
25
|
+
/**
|
|
26
|
+
* Builds a typed JSONB Drizzle custom type backed by an Effect Schema.
|
|
27
|
+
*
|
|
28
|
+
* Values are stored as PostgreSQL `jsonb`. The schema validates on both
|
|
29
|
+
* read (`fromDriver`) and write (`toDriver`): writing encodes through the
|
|
30
|
+
* schema and then through {@link JsonText}; reading decodes unknown driver
|
|
31
|
+
* output straight through the schema.
|
|
32
|
+
*
|
|
33
|
+
* The driver this library ships a Postgres binding for (node-pg, via
|
|
34
|
+
* `@effect/sql-pg`) already parses `jsonb` columns before `fromDriver` sees
|
|
35
|
+
* them, so the `typeof value === "string"` branch below is dead under that
|
|
36
|
+
* driver — it exists for a driver that hands back raw JSON text instead,
|
|
37
|
+
* and is exercised directly by this module's tests.
|
|
38
|
+
*
|
|
39
|
+
* Nullable columns are handled transparently — `null` values pass through
|
|
40
|
+
* Drizzle without reaching this codec, so `.nullable()` is not required on
|
|
41
|
+
* `schema`.
|
|
42
|
+
*
|
|
43
|
+
* `schema` is bound to `Schema.Codec<unknown, unknown, never, never>` rather
|
|
44
|
+
* than the wider `Schema.Top`: `toDriver` / `fromDriver` are synchronous
|
|
45
|
+
* Drizzle hooks with no Effect context to satisfy a requirement from, so a
|
|
46
|
+
* schema this codec accepts must already need no services to decode or
|
|
47
|
+
* encode.
|
|
48
|
+
*/
|
|
49
|
+
const jsonbSchema = (schema) => (name) => {
|
|
50
|
+
const encode = Schema.encodeSync(schema);
|
|
51
|
+
const decode = Schema.decodeUnknownSync(schema);
|
|
52
|
+
return customType({
|
|
53
|
+
dataType() {
|
|
54
|
+
return "jsonb";
|
|
55
|
+
},
|
|
56
|
+
toDriver(value) {
|
|
57
|
+
return encodeJsonText(encode(value));
|
|
58
|
+
},
|
|
59
|
+
fromDriver(value) {
|
|
60
|
+
if (!Predicate.isString(value)) return decode(value);
|
|
61
|
+
return Option.match(decodeJsonText(value), {
|
|
62
|
+
onNone: () => decode(value),
|
|
63
|
+
onSome: (parsed) => decode(parsed)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
})(name);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Custom column type for PostgreSQL `tsvector`.
|
|
70
|
+
*
|
|
71
|
+
* Used for generated full-text search columns. The value is opaque to the
|
|
72
|
+
* consuming application — queries use `sql` template literals for matching.
|
|
73
|
+
*/
|
|
74
|
+
const tsvector = (name) => customType({ dataType() {
|
|
75
|
+
return "tsvector";
|
|
76
|
+
} })(name);
|
|
77
|
+
/**
|
|
78
|
+
* Custom column type for PostgreSQL `bytea` (binary data).
|
|
79
|
+
*
|
|
80
|
+
* Typed `Uint8Array` rather than `Buffer` — see {@link byteaToHex} for why
|
|
81
|
+
* the Node global is off-limits here. Both directions are honest about the
|
|
82
|
+
* runtime: `@effect/sql-pg`'s binary `bytea` codec decodes to a `Uint8Array`
|
|
83
|
+
* and encodes any `Uint8Array` (a Node `Buffer` included, as a subclass) as
|
|
84
|
+
* raw bytes, so a plain `Uint8Array` binds identically to a `Buffer`.
|
|
85
|
+
*/
|
|
86
|
+
const bytea = (name) => customType({ dataType() {
|
|
87
|
+
return "bytea";
|
|
88
|
+
} })(name);
|
|
89
|
+
/**
|
|
90
|
+
* A fresh `created_at` / `updated_at` pair: `timestamptz`, `NOT NULL`,
|
|
91
|
+
* defaulting to `now()`.
|
|
92
|
+
*
|
|
93
|
+
* A factory, never a shared object: Drizzle column builders are
|
|
94
|
+
* **mutable** — `.notNull()` and `.defaultNow()` mutate and return the same
|
|
95
|
+
* builder instance. A module-level constant reused across tables would
|
|
96
|
+
* therefore hand every table the same two builders, and whichever table
|
|
97
|
+
* was declared last would win any later refinement. Calling `timestamps()`
|
|
98
|
+
* per table mints a fresh pair.
|
|
99
|
+
*
|
|
100
|
+
* `updated_at` defaults rather than auto-updates — nothing in Postgres
|
|
101
|
+
* bumps it, the write path does.
|
|
102
|
+
*
|
|
103
|
+
* Spread rather than a base table: Drizzle records a table's columns in
|
|
104
|
+
* declaration order, and that order is what a consumer's `db:generate`
|
|
105
|
+
* emits as the `CREATE TABLE` column order. Spreading at the position two
|
|
106
|
+
* hand-written lines occupied keeps generated SQL byte-identical, so
|
|
107
|
+
* adopting this helper does not move a baseline migration.
|
|
108
|
+
*/
|
|
109
|
+
const timestamps = () => ({
|
|
110
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
111
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Renders a `bytea` value (typically a content hash) as its lowercase hex
|
|
115
|
+
* string.
|
|
116
|
+
*
|
|
117
|
+
* Written against `Uint8Array` rather than `Buffer.prototype.toString("hex")`
|
|
118
|
+
* on purpose: a runtime that declares the Node `Buffer` global alongside
|
|
119
|
+
* `@types/node` (Cloudflare Workers' `nodejs_compat` types are one example)
|
|
120
|
+
* can strip the Node-only methods off the global `Buffer` interface, or
|
|
121
|
+
* degrade the `Buffer` *value* to `any`, so every `Buffer.*` call goes
|
|
122
|
+
* unchecked with no signal at all. `Uint8Array` is what a `bytea` row value
|
|
123
|
+
* structurally is on every runtime, so this encodes identically everywhere
|
|
124
|
+
* without depending on that global.
|
|
125
|
+
*/
|
|
126
|
+
const byteaToHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
127
|
+
/**
|
|
128
|
+
* Decodes a hex string into the `Uint8Array` a `bytea` column binds — the
|
|
129
|
+
* inverse of {@link byteaToHex}, replacing `Buffer.from(hex, "hex")`.
|
|
130
|
+
*
|
|
131
|
+
* Strict where `Buffer.from` is silent: `Buffer.from` decodes the leading
|
|
132
|
+
* valid pairs and drops the rest, so a malformed hash quietly becomes a
|
|
133
|
+
* short-but-plausible key that matches no row. This throws instead — a
|
|
134
|
+
* caller that takes a hash from outside the trust boundary is expected to
|
|
135
|
+
* validate its shape before calling this, and gets a loud failure instead
|
|
136
|
+
* of a silently truncated key when it doesn't.
|
|
137
|
+
*
|
|
138
|
+
* @throws TypeError if `hex` is not an even-length run of hex digits.
|
|
139
|
+
*/
|
|
140
|
+
const hexToBytea = (hex) => {
|
|
141
|
+
if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) throw new TypeError(`Expected an even-length hex string, received ${encodeJsonText(hex)}`);
|
|
142
|
+
return Uint8Array.from({ length: hex.length / 2 }, (_unused, index) => Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16));
|
|
143
|
+
};
|
|
144
|
+
export { bytea, byteaToHex, hexToBytea, jsonbSchema, timestamps, tsvector };
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=columns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"columns.js","names":[],"sources":["../src/columns.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4BA,MAAM,WAAW,OAAO,eAAe,OAAO,OAAO;AACrD,MAAM,iBAAiB,OAAO,WAAW,QAAQ;AACjD,MAAM,iBAAiB,OAAO,oBAAoB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;AA0B1D,MAAa,eAC8C,YAEvD,SACyF;CACzF,MAAM,SAAS,OAAO,WAAW,MAAM;CACvC,MAAM,SAAS,OAAO,kBAAkB,MAAM;CAC9C,OAAO,WAAqD;EAC1D,WAAW;GACT,OAAO;EACT;EACA,SAAS,OAAO;GACd,OAAO,eAAe,OAAO,KAAK,CAAC;EACrC;EACA,WAAW,OAAO;GAChB,IAAI,CAAC,UAAU,SAAS,KAAK,GAAG,OAAO,OAAO,KAAK;GACnD,OAAO,OAAO,MAAM,eAAe,KAAK,GAAG;IACzC,cAAc,OAAO,KAAK;IAC1B,SAAS,WAAW,OAAO,MAAM;GACnC,CAAC;EACH;CACF,CAAC,CAAC,CAAC,IAAI;AACT;;;;;;;AAQF,MAAa,YACX,SAEA,WAAiD,EAC/C,WAAW;CACT,OAAO;AACT,EACF,CAAC,CAAC,CAAC,IAAI;;;;;;;;;;AAWT,MAAa,SACX,SAEA,WAAyD,EACvD,WAAW;CACT,OAAO;AACT,EACF,CAAC,CAAC,CAAC,IAAI;;;;;;;;;;;;;;;;;;;;;AAiCT,MAAa,oBAAsC;CACjD,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;CAChF,WAAW,UAAU,cAAc,EAAE,cAAc,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAClF;;;;;;;;;;;;;;AAeA,MAAa,cAAc,UACzB,MAAM,KAAK,QAAQ,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;;;;;;;;;;;;;;AAezE,MAAa,cAAc,QAA4B;CACrD,IAAI,IAAI,SAAS,MAAM,KAAK,CAAC,iBAAiB,KAAK,GAAG,GACpD,MAAM,IAAI,UAAU,gDAAgD,eAAe,GAAG,GAAG;CAE3F,OAAO,WAAW,KAAK,EAAE,QAAQ,IAAI,SAAS,EAAE,IAAI,SAAS,UAC3D,OAAO,SAAS,IAAI,MAAM,QAAQ,GAAG,QAAQ,IAAI,CAAC,GAAG,EAAE,CACzD;AACF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { AnyPgColumn, ExtraConfigColumn, PgTableExtraConfigValue } from "drizzle-orm/pg-core";
|
|
2
|
+
/**
|
|
3
|
+
* `CHECK (column IN ('a', 'b', …))`, named `name`.
|
|
4
|
+
*
|
|
5
|
+
* The models declare each vocabulary once as an exported `as const` array and
|
|
6
|
+
* validate the wire against `Schema.Literals` over it; the database restates
|
|
7
|
+
* the same array as an `IN (...)` CHECK so a row written by any path — a
|
|
8
|
+
* batched ingestion upsert included — cannot carry a value the API would refuse
|
|
9
|
+
* to serve. There is no Postgres enum type: a `text` column plus a CHECK is
|
|
10
|
+
* what lets a vocabulary grow with one migration (`ALTER … DROP/ADD
|
|
11
|
+
* CONSTRAINT`) instead of an `ALTER TYPE … ADD VALUE` that cannot run inside a
|
|
12
|
+
* transaction.
|
|
13
|
+
*
|
|
14
|
+
* A `NULL` in the column **passes** — `NULL IN (...)` is unknown, which CHECK
|
|
15
|
+
* accepts — so the same constraint serves nullable vocabularies without an
|
|
16
|
+
* `IS NULL OR` guard. Nullability is the column's own `NOT NULL` to declare.
|
|
17
|
+
*
|
|
18
|
+
* The values are rendered as literal SQL text, not bind parameters: a CHECK
|
|
19
|
+
* expression lives in the migration file, where a `$1` placeholder has nothing
|
|
20
|
+
* to bind to. They come straight from the models' `as const` arrays, so the two
|
|
21
|
+
* vocabularies cannot drift — a model change shows up as schema drift in the
|
|
22
|
+
* consumer's migration-drift check until the migration is regenerated.
|
|
23
|
+
*/
|
|
24
|
+
export declare const enumCheck: (name: string, column: ExtraConfigColumn, values: readonly string[]) => PgTableExtraConfigValue;
|
|
25
|
+
/** A stable table contributes its `id` + `type` columns to the discriminator. */
|
|
26
|
+
export interface TypedStable {
|
|
27
|
+
readonly id: AnyPgColumn;
|
|
28
|
+
readonly type: AnyPgColumn;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Constraint-name templates, overriding the defaults
|
|
32
|
+
* `${level}_id_type_unique`, `${level}_id_type_fk` and `${level}_type_valid`.
|
|
33
|
+
* A consumer adopting this helper over hand-written constraints pins whatever
|
|
34
|
+
* names its existing migrations already carry, so the switch renames nothing.
|
|
35
|
+
*/
|
|
36
|
+
export interface DiscriminatorNames {
|
|
37
|
+
/** Name of the `UNIQUE(id, type)` constraint. */
|
|
38
|
+
readonly unique?: ((level: string) => string) | undefined;
|
|
39
|
+
/** Name of the composite `(id, type)` → parent `(id, type)` foreign key. */
|
|
40
|
+
readonly parentFk?: ((level: string) => string) | undefined;
|
|
41
|
+
/** Name of the per-level `type` CHECK. */
|
|
42
|
+
readonly check?: ((level: string) => string) | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The three constraints that enforce a threaded `type` discriminator, bound to
|
|
46
|
+
* one hierarchy. Generic over the hierarchy's level union `Level`, so a level
|
|
47
|
+
* the consumer has no leaf set for is a type error at the call site.
|
|
48
|
+
*/
|
|
49
|
+
export interface DiscriminatorToolkit<Level extends string> {
|
|
50
|
+
/**
|
|
51
|
+
* `UNIQUE(id, type)` — the target every child's composite `(id, type)` FK
|
|
52
|
+
* needs. `level` is a plain `string`, not `Level`: a root table sitting
|
|
53
|
+
* *above* the ontology's levels (and so not itself a level) can carry it too.
|
|
54
|
+
*/
|
|
55
|
+
readonly typeUnique: (level: string, stable: TypedStable) => PgTableExtraConfigValue;
|
|
56
|
+
/**
|
|
57
|
+
* The composite `(id, type)` → parent `(id, type)` foreign key. `onDelete` is
|
|
58
|
+
* left to the plain single-column `id` FK the table already carries
|
|
59
|
+
* (cascade); this arc only enforces the discriminator match.
|
|
60
|
+
*
|
|
61
|
+
* `onUpdate: "cascade"` (via {@link DiscriminatorToolkit.typeConstraints}'
|
|
62
|
+
* `refinable` option) makes a bare parent row **refinable**: an operation
|
|
63
|
+
* that flips the root's `type` from a generic value to a concrete leaf
|
|
64
|
+
* cascades the new `type` down the pre-existing `(id, type)` chain before the
|
|
65
|
+
* concrete rows are inserted — so the flip is a plain ordered transaction, no
|
|
66
|
+
* `DEFERRABLE` constraint needed. Branches that don't refine keep the default
|
|
67
|
+
* (no referential action on this arc).
|
|
68
|
+
*/
|
|
69
|
+
readonly typeParentFk: (level: Level, stable: TypedStable, parent: TypedStable, onUpdate?: "cascade") => PgTableExtraConfigValue;
|
|
70
|
+
/**
|
|
71
|
+
* The per-level `type` CHECK. A leaf admits only its own type; an
|
|
72
|
+
* intermediate or branch root admits every concrete leaf in its subtree (the
|
|
73
|
+
* `leaves` callback the toolkit was built with). The value list is
|
|
74
|
+
* parameterised, not interpolated.
|
|
75
|
+
*
|
|
76
|
+
* A childless marker — an ontology level with no concrete leaf beneath it yet
|
|
77
|
+
* — has an empty leaf set, so it admits no `type` at all: no leaf names it as
|
|
78
|
+
* an ancestor, so nothing ever materialises a row there and the table stays
|
|
79
|
+
* empty until the ontology grows a leaf under it. Emits `CHECK (false)` for
|
|
80
|
+
* that case — `type IN ()` is a syntax error, and `false` correctly marks the
|
|
81
|
+
* level uninstantiable rather than admitting an arbitrary value.
|
|
82
|
+
*/
|
|
83
|
+
readonly typeCheck: (level: Level, stable: TypedStable) => PgTableExtraConfigValue;
|
|
84
|
+
/**
|
|
85
|
+
* All three discriminator constraints for a non-root level:
|
|
86
|
+
* `UNIQUE(id, type)`, the composite `(id, type)` → parent FK, and the
|
|
87
|
+
* per-level CHECK. A branch root passes the table above it as `parent`; the
|
|
88
|
+
* root table itself gets only {@link DiscriminatorToolkit.typeUnique} (no
|
|
89
|
+
* parent, no CHECK — it admits any leaf).
|
|
90
|
+
*
|
|
91
|
+
* `refinable: true` gives the composite FK `ON UPDATE CASCADE` — see
|
|
92
|
+
* {@link DiscriminatorToolkit.typeParentFk}.
|
|
93
|
+
*/
|
|
94
|
+
readonly typeConstraints: (level: Level, stable: TypedStable, parent: TypedStable, options?: {
|
|
95
|
+
readonly refinable?: boolean | undefined;
|
|
96
|
+
}) => PgTableExtraConfigValue[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build a {@link DiscriminatorToolkit} for one hierarchy.
|
|
100
|
+
*
|
|
101
|
+
* `leaves(level)` is the seam the ontology plugs into: it answers the concrete
|
|
102
|
+
* leaf `type` values admissible at `level` (its subtree's leaves — for a leaf
|
|
103
|
+
* level, itself). The library carries no level ontology of its own, so this
|
|
104
|
+
* callback, and the optional {@link DiscriminatorNames} templates, are the
|
|
105
|
+
* whole configuration.
|
|
106
|
+
*
|
|
107
|
+
* The constraints it emits are what make a sibling-subtype chimera
|
|
108
|
+
* unrepresentable: `UNIQUE(id, type)` gives children a composite key to point
|
|
109
|
+
* at, the composite FK arcs the discriminator up one level (a child can only
|
|
110
|
+
* exist under a parent carrying the SAME `type`), and the CHECK narrows `type`
|
|
111
|
+
* to the level's own subtree.
|
|
112
|
+
*/
|
|
113
|
+
export declare const makeDiscriminatorConstraints: <Level extends string>(options: {
|
|
114
|
+
readonly leaves: (level: Level) => ReadonlyArray<string>;
|
|
115
|
+
readonly names?: DiscriminatorNames | undefined;
|
|
116
|
+
}) => DiscriminatorToolkit<Level>;
|
|
117
|
+
//# sourceMappingURL=constraints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints.d.ts","names":[],"sources":["../src/constraints.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;qBAsDa,YAAS,cACR,QACJ,mBAAiB,8BAExB;;iBAMc;WACN,IAAI;WACJ,MAAM;;;;;;;;iBAmBA;;WAEN,WAAW;;WAEX,aAAa;;WAEb,UAAU;;;;;;;iBAQJ,qBAAqB;;;;;;WAM3B,aAAa,eAAe,QAAQ,gBAAgB;;;;;;;;;;;;;;WAcpD,eACP,OAAO,OACP,QAAQ,aACR,QAAQ,aACR,yBACG;;;;;;;;;;;;;;WAcI,YAAY,OAAO,OAAO,QAAQ,gBAAgB;;;;;;;;;;;WAWlD,kBACP,OAAO,OACP,QAAQ,aACR,QAAQ,aACR;aAAqB;QAClB;;;;;;;;;;;;;;;;;qBAkBM,+BAAgC,sBAAoB;WACtD,SAAS,OAAO,UAAU;WAC1B,QAAQ;MACf,qBAAqB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Array from "effect/Array";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { check, foreignKey, unique } from "drizzle-orm/pg-core";
|
|
4
|
+
// src/constraints.ts
|
|
5
|
+
/**
|
|
6
|
+
* @copyright Thomas Foster
|
|
7
|
+
*
|
|
8
|
+
* **Table-level CHECK / UNIQUE / composite-FK helpers.**
|
|
9
|
+
*
|
|
10
|
+
* Two independent families, both emitting values for a table factory's
|
|
11
|
+
* `extraConstraints` seam:
|
|
12
|
+
*
|
|
13
|
+
* - {@link enumCheck} — the `CHECK (col IN (…))` behind an enum-valued `text`
|
|
14
|
+
* column, so a vocabulary declared once in a model constrains the database
|
|
15
|
+
* too.
|
|
16
|
+
* - {@link makeDiscriminatorConstraints} — the three constraints that thread a
|
|
17
|
+
* `type` discriminator up a hierarchy of stable tables, parameterised over
|
|
18
|
+
* the hierarchy (the library knows no level ontology; the consumer supplies
|
|
19
|
+
* the leaf set per level).
|
|
20
|
+
*
|
|
21
|
+
* @module constraints
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* `CHECK (column IN ('a', 'b', …))`, named `name`.
|
|
25
|
+
*
|
|
26
|
+
* The models declare each vocabulary once as an exported `as const` array and
|
|
27
|
+
* validate the wire against `Schema.Literals` over it; the database restates
|
|
28
|
+
* the same array as an `IN (...)` CHECK so a row written by any path — a
|
|
29
|
+
* batched ingestion upsert included — cannot carry a value the API would refuse
|
|
30
|
+
* to serve. There is no Postgres enum type: a `text` column plus a CHECK is
|
|
31
|
+
* what lets a vocabulary grow with one migration (`ALTER … DROP/ADD
|
|
32
|
+
* CONSTRAINT`) instead of an `ALTER TYPE … ADD VALUE` that cannot run inside a
|
|
33
|
+
* transaction.
|
|
34
|
+
*
|
|
35
|
+
* A `NULL` in the column **passes** — `NULL IN (...)` is unknown, which CHECK
|
|
36
|
+
* accepts — so the same constraint serves nullable vocabularies without an
|
|
37
|
+
* `IS NULL OR` guard. Nullability is the column's own `NOT NULL` to declare.
|
|
38
|
+
*
|
|
39
|
+
* The values are rendered as literal SQL text, not bind parameters: a CHECK
|
|
40
|
+
* expression lives in the migration file, where a `$1` placeholder has nothing
|
|
41
|
+
* to bind to. They come straight from the models' `as const` arrays, so the two
|
|
42
|
+
* vocabularies cannot drift — a model change shows up as schema drift in the
|
|
43
|
+
* consumer's migration-drift check until the migration is regenerated.
|
|
44
|
+
*/
|
|
45
|
+
const enumCheck = (name, column, values) => check(name, sql`${column} IN (${sql.raw(values.map((value) => `'${value}'`).join(", "))})`);
|
|
46
|
+
/**
|
|
47
|
+
* Build a {@link DiscriminatorToolkit} for one hierarchy.
|
|
48
|
+
*
|
|
49
|
+
* `leaves(level)` is the seam the ontology plugs into: it answers the concrete
|
|
50
|
+
* leaf `type` values admissible at `level` (its subtree's leaves — for a leaf
|
|
51
|
+
* level, itself). The library carries no level ontology of its own, so this
|
|
52
|
+
* callback, and the optional {@link DiscriminatorNames} templates, are the
|
|
53
|
+
* whole configuration.
|
|
54
|
+
*
|
|
55
|
+
* The constraints it emits are what make a sibling-subtype chimera
|
|
56
|
+
* unrepresentable: `UNIQUE(id, type)` gives children a composite key to point
|
|
57
|
+
* at, the composite FK arcs the discriminator up one level (a child can only
|
|
58
|
+
* exist under a parent carrying the SAME `type`), and the CHECK narrows `type`
|
|
59
|
+
* to the level's own subtree.
|
|
60
|
+
*/
|
|
61
|
+
const makeDiscriminatorConstraints = (options) => {
|
|
62
|
+
const uniqueName = options.names?.unique ?? ((level) => `${level}_id_type_unique`);
|
|
63
|
+
const parentFkName = options.names?.parentFk ?? ((level) => `${level}_id_type_fk`);
|
|
64
|
+
const checkName = options.names?.check ?? ((level) => `${level}_type_valid`);
|
|
65
|
+
const typeUnique = (level, stable) => unique(uniqueName(level)).on(stable.id, stable.type);
|
|
66
|
+
const typeParentFk = (level, stable, parent, onUpdate) => {
|
|
67
|
+
const fk = foreignKey({
|
|
68
|
+
name: parentFkName(level),
|
|
69
|
+
columns: [stable.id, stable.type],
|
|
70
|
+
foreignColumns: [parent.id, parent.type]
|
|
71
|
+
});
|
|
72
|
+
return onUpdate === "cascade" ? fk.onUpdate("cascade") : fk;
|
|
73
|
+
};
|
|
74
|
+
const typeCheck = (level, stable) => {
|
|
75
|
+
const leaves = options.leaves(level);
|
|
76
|
+
if (Array.isReadonlyArrayEmpty(leaves)) return check(checkName(level), sql`false`);
|
|
77
|
+
const list = sql.join(leaves.map((leaf) => sql`${leaf}`), sql`, `);
|
|
78
|
+
return check(checkName(level), sql`${stable.type} IN (${list})`);
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
typeUnique,
|
|
82
|
+
typeParentFk,
|
|
83
|
+
typeCheck,
|
|
84
|
+
typeConstraints: (level, stable, parent, { refinable = false } = {}) => [
|
|
85
|
+
typeUnique(level, stable),
|
|
86
|
+
typeParentFk(level, stable, parent, refinable ? "cascade" : void 0),
|
|
87
|
+
typeCheck(level, stable)
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export { enumCheck, makeDiscriminatorConstraints };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=constraints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints.js","names":[],"sources":["../src/constraints.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,aACX,MACA,QACA,WAEA,MAAM,MAAM,GAAG,GAAG,OAAO,OAAO,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,EAAE;;;;;;;;;;;;;;;;AAiH5F,MAAa,gCAAsD,YAGhC;CACjC,MAAM,aAAa,QAAQ,OAAO,YAAY,UAAkB,GAAG,MAAM;CACzE,MAAM,eAAe,QAAQ,OAAO,cAAc,UAAkB,GAAG,MAAM;CAC7E,MAAM,YAAY,QAAQ,OAAO,WAAW,UAAkB,GAAG,MAAM;CAEvE,MAAM,cAAc,OAAe,WACjC,OAAO,WAAW,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,OAAO,IAAI;CAErD,MAAM,gBACJ,OACA,QACA,QACA,aAC4B;EAC5B,MAAM,KAAK,WAAW;GACpB,MAAM,aAAa,KAAK;GACxB,SAAS,CAAC,OAAO,IAAI,OAAO,IAAI;GAChC,gBAAgB,CAAC,OAAO,IAAI,OAAO,IAAI;EACzC,CAAC;EACD,OAAO,aAAa,YAAY,GAAG,SAAS,SAAS,IAAI;CAC3D;CAEA,MAAM,aAAa,OAAc,WAAiD;EAChF,MAAM,SAAS,QAAQ,OAAO,KAAK;EACnC,IAAI,MAAM,qBAAqB,MAAM,GAAG,OAAO,MAAM,UAAU,KAAK,GAAG,GAAG,OAAO;EACjF,MAAM,OAAO,IAAI,KACf,OAAO,KAAK,SAAS,GAAG,GAAG,MAAM,GACjC,GAAG,IACL;EACA,OAAO,MAAM,UAAU,KAAK,GAAG,GAAG,GAAG,OAAO,KAAK,OAAO,KAAK,EAAE;CACjE;CAEA,OAAO;EACL;EACA;EACA;EACA,kBACE,OACA,QACA,QACA,EAAE,YAAY,UAAwD,CAAC,MACzC;GAC9B,WAAW,OAAO,MAAM;GACxB,aAAa,OAAO,QAAQ,QAAQ,YAAY,YAAY,KAAA,CAAS;GACrE,UAAU,OAAO,MAAM;EACzB;CACF;AACF"}
|