@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +508 -2
  3. package/dist/columns.d.ts +113 -0
  4. package/dist/columns.d.ts.map +1 -0
  5. package/dist/columns.js +146 -0
  6. package/dist/columns.js.map +1 -0
  7. package/dist/constraints.d.ts +117 -0
  8. package/dist/constraints.d.ts.map +1 -0
  9. package/dist/constraints.js +93 -0
  10. package/dist/constraints.js.map +1 -0
  11. package/dist/database.d.ts +189 -0
  12. package/dist/database.d.ts.map +1 -0
  13. package/dist/database.js +51 -0
  14. package/dist/database.js.map +1 -0
  15. package/dist/derive.d.ts +97 -0
  16. package/dist/derive.d.ts.map +1 -0
  17. package/dist/derive.js +142 -0
  18. package/dist/derive.js.map +1 -0
  19. package/dist/errors.d.ts +157 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +287 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index-CFhgcF4N.d.ts +418 -0
  24. package/dist/index-CFhgcF4N.d.ts.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +1 -0
  27. package/dist/jsonapi/derive.d.ts +28 -0
  28. package/dist/jsonapi/derive.d.ts.map +1 -0
  29. package/dist/jsonapi/derive.js +28 -0
  30. package/dist/jsonapi/derive.js.map +1 -0
  31. package/dist/jsonapi/index.d.ts +189 -0
  32. package/dist/jsonapi/index.d.ts.map +1 -0
  33. package/dist/jsonapi/index.js +164 -0
  34. package/dist/jsonapi/index.js.map +1 -0
  35. package/dist/query.d.ts +111 -0
  36. package/dist/query.d.ts.map +1 -0
  37. package/dist/query.js +142 -0
  38. package/dist/query.js.map +1 -0
  39. package/dist/references.d.ts +38 -0
  40. package/dist/references.d.ts.map +1 -0
  41. package/dist/references.js +60 -0
  42. package/dist/references.js.map +1 -0
  43. package/dist/sort.d.ts +129 -0
  44. package/dist/sort.d.ts.map +1 -0
  45. package/dist/sort.js +225 -0
  46. package/dist/sort.js.map +1 -0
  47. package/dist/src-CBeuhy5b.js +478 -0
  48. package/dist/src-CBeuhy5b.js.map +1 -0
  49. package/dist/update.d.ts +61 -0
  50. package/dist/update.d.ts.map +1 -0
  51. package/dist/update.js +64 -0
  52. package/dist/update.js.map +1 -0
  53. package/dist/versioned-chain.d.ts +524 -0
  54. package/dist/versioned-chain.d.ts.map +1 -0
  55. package/dist/versioned-chain.js +298 -0
  56. package/dist/versioned-chain.js.map +1 -0
  57. package/package.json +102 -4
  58. package/src/columns.ts +186 -0
  59. package/src/constraints.ts +225 -0
  60. package/src/database.ts +273 -0
  61. package/src/derivation-error.ts +27 -0
  62. package/src/derive.ts +320 -0
  63. package/src/errors.ts +466 -0
  64. package/src/index.ts +653 -0
  65. package/src/jsonapi/derive.ts +74 -0
  66. package/src/jsonapi/index.ts +355 -0
  67. package/src/query.ts +183 -0
  68. package/src/references.ts +103 -0
  69. package/src/sort.ts +368 -0
  70. package/src/tree.ts +367 -0
  71. package/src/update.ts +88 -0
  72. package/src/versioned-chain.ts +1027 -0
package/src/derive.ts ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @copyright Thomas Foster
3
+ *
4
+ * **Effect Schema → Drizzle derivation: the derivation entry (Drizzle).**
5
+ *
6
+ * Reads the {@link DbColumn} metadata a schema declaration carries (via
7
+ * {@link fieldColumns} from the {@link ./index.ts} annotation entry, or via any
8
+ * other projection that yields a `Record<string, DbColumn>`) and emits the
9
+ * matching Drizzle column builders. Depends on Drizzle, so a model / client
10
+ * layer importing only the annotation entry never pulls the ORM.
11
+ *
12
+ * **Extensible column kinds.** The kind → builder mapping is *injected*, not
13
+ * baked in: a consumer passes a builder registry (a record from its own kind
14
+ * union to a Drizzle column builder) to {@link makeDeriveColumns}, and the
15
+ * derivation preserves each column's precise builder type — so the derived
16
+ * column map keeps `$inferSelect` typed — by reading the type-level phantom
17
+ * `dbColumn` stamps alongside the runtime annotation.
18
+ *
19
+ * **Two entry shapes, one interpreter.** {@link makeDeriveColumns} returns both
20
+ * `fromFields` (a `Schema.Struct`'s fields, the generic source) and `fromMeta`
21
+ * (an already-projected metadata map, the seam the `./jsonapi/derive` entry and
22
+ * any other projection build on). They share one builder loop.
23
+ *
24
+ * **Failures here are THROWN, not Effect failures.** Everything in this module
25
+ * runs at *module load*, while a consumer's table modules are being evaluated —
26
+ * before any Effect runtime exists to run a failure in, and before any query
27
+ * can be issued. A misdeclared table must fail at `import`, not at first
28
+ * insert, so a missing builder / a missing reference hook / bounds on a column
29
+ * the table does not carry throw {@link DerivationError}. Runtime query
30
+ * failures are the opposite and are never thrown: they travel in the Effect
31
+ * error channel as the tagged errors of the `./errors` entry.
32
+ *
33
+ * @module derive
34
+ */
35
+
36
+ import { sql, type ColumnBuilderBase, type SQL } from "drizzle-orm";
37
+ import {
38
+ check,
39
+ type AnyPgColumn,
40
+ type ExtraConfigColumn,
41
+ type PgTableExtraConfigValue,
42
+ } from "drizzle-orm/pg-core";
43
+ import * as Arr from "effect/Array";
44
+ import * as R from "effect/Record";
45
+ import * as Schema from "effect/Schema";
46
+ import { DerivationError } from "./derivation-error.ts";
47
+ import {
48
+ fieldColumnBounds,
49
+ fieldColumns,
50
+ type ColumnBounds,
51
+ type DbColumn,
52
+ type DbColumnReference,
53
+ type FieldColumnsMetaOf,
54
+ } from "./index.ts";
55
+
56
+ /**
57
+ * A construction-time derivation failure, thrown at module load when a table
58
+ * cannot be built from its declarations. The same class the Drizzle-free `.`
59
+ * entry exports (for `defineTree`), re-exported here for the table derivations.
60
+ */
61
+ export { DerivationError };
62
+
63
+ /**
64
+ * A registry mapping column kinds to Drizzle column builders — one builder per
65
+ * kind, taking the column name and returning a fresh (nullable) builder. The
66
+ * consumer supplies this; the package provides no fixed set.
67
+ */
68
+ export type ColumnBuilders<Kind extends string = string> = Record<
69
+ Kind,
70
+ (column: string) => ColumnBuilderBase
71
+ >;
72
+
73
+ /** The (nullable) builder a kind produces in registry `B`. */
74
+ type Builder<B extends ColumnBuilders, P extends keyof B> = ReturnType<B[P]>;
75
+
76
+ /** The `.notNull()` form of a builder. */
77
+ type NotNull<X> = X extends { notNull(): infer R } ? R : X;
78
+
79
+ /**
80
+ * The Drizzle column builder a single db-column annotation produces against
81
+ * registry `B`. Total over any `M` (a widened `unknown` metadata falls back to
82
+ * the base builder type), computing the precise builder type for a concrete
83
+ * `{ pg, notNull }` metadata.
84
+ *
85
+ * Exported so a projection-specific derivation (the `./jsonapi/derive` entry)
86
+ * can name this type without restating it.
87
+ */
88
+ export type BuilderFor<M, B extends ColumnBuilders> = M extends {
89
+ readonly pg: infer P;
90
+ readonly notNull: infer N;
91
+ }
92
+ ? P extends keyof B
93
+ ? N extends true
94
+ ? NotNull<Builder<B, P>>
95
+ : Builder<B, P>
96
+ : ColumnBuilderBase
97
+ : ColumnBuilderBase;
98
+
99
+ /**
100
+ * The Drizzle column builders a db-column metadata map produces against
101
+ * registry `B`, keyed and typed per entry — the type-level counterpart of the
102
+ * builder loop {@link makeDeriveColumns} returns.
103
+ */
104
+ export type DerivedColumns<Meta, B extends ColumnBuilders> = {
105
+ readonly [K in keyof Meta]: BuilderFor<Meta[K], B>;
106
+ };
107
+
108
+ /*
109
+ * **The one statement of the `?: T | undefined` rule.** Every optional member
110
+ * of a config type this library hands to Drizzle — `resolveReference` below,
111
+ * `DiscriminatorNames`' templates in ./constraints.ts, the annotation entry's
112
+ * `DbColumn.default` / `.references` / `.checkName` — is spelled
113
+ * `?: T | undefined`, never `Option`. Drizzle reads `undefined` as "not
114
+ * supplied"; handed an `Option` it would emit the wrapper object as a literal
115
+ * value (a DEFAULT of `{_tag: "None"}`, a referential action of `[Object]`) —
116
+ * wrong DDL that no type error and no unit test catches, because Drizzle's
117
+ * config members are typed loosely enough to accept it. Readers this library
118
+ * owns (`readDbColumn`, `readDbTable`, `findPgError`, …) return `Option`; the
119
+ * conversion happens at their boundary, not in Drizzle-facing config.
120
+ */
121
+ /* oxlint-disable effect/prefer-option-over-null -- Drizzle-facing config: Drizzle reads `undefined` as "not supplied" */
122
+
123
+ /**
124
+ * Optional hooks for {@link makeDeriveColumns}. `resolveReference` turns a
125
+ * column's {@link DbColumnReference} name pair into the Drizzle column thunk
126
+ * `.references()` needs — the seam that lets FK-backed attributes derive their
127
+ * column instead of being hand-authored. Without it, a `references` annotation
128
+ * is a configuration error (throws), so a consumer only wires it when it
129
+ * declares FK columns. The `./references` entry ships one implementation.
130
+ */
131
+ export interface DeriveOptions {
132
+ readonly resolveReference?: ((reference: DbColumnReference) => () => AnyPgColumn) | undefined;
133
+ }
134
+
135
+ /* oxlint-enable effect/prefer-option-over-null */
136
+
137
+ /**
138
+ * A fluent view of a Drizzle column builder — the chainable `.notNull()` /
139
+ * `.default()` / `.references()` methods the registry's widened
140
+ * `ColumnBuilderBase` return type hides. Runtime-real; used only to type the
141
+ * dynamic build chain. Typed to exactly what the chain passes.
142
+ */
143
+ interface FluentColumn extends ColumnBuilderBase {
144
+ notNull(): FluentColumn;
145
+ default(value: NonNullable<DbColumn["default"]>): FluentColumn;
146
+ references(
147
+ ref: () => AnyPgColumn,
148
+ actions: Pick<DbColumnReference, "onDelete" | "onUpdate">,
149
+ ): FluentColumn;
150
+ }
151
+
152
+ /**
153
+ * The derivation pair {@link makeDeriveColumns} returns for builder registry
154
+ * `B`: `fromMeta` over an already-projected metadata map, `fromFields` over a
155
+ * `Schema.Struct`'s fields. Both throw {@link DerivationError} at module load
156
+ * on a misdeclared column.
157
+ */
158
+ export interface DeriveColumns<B extends ColumnBuilders> {
159
+ /** Derive the builders an already-projected metadata map declares. */
160
+ readonly fromMeta: <Meta extends Record<string, DbColumn>>(
161
+ metas: Meta,
162
+ ) => DerivedColumns<Meta, B>;
163
+ /** Derive the builders a `Schema.Struct`'s annotated fields declare. */
164
+ readonly fromFields: <F extends Schema.Struct.Fields>(
165
+ fields: F,
166
+ ) => DerivedColumns<FieldColumnsMetaOf<F>, B>;
167
+ }
168
+
169
+ /**
170
+ * Build a derivation bound to a specific builder registry. The returned pair
171
+ * interprets per-column `dbColumn` metadata into the Drizzle column builders a
172
+ * table factory's `columns` expects:
173
+ *
174
+ * - `fromFields` takes a `Schema.Struct`'s fields directly (peeling the
175
+ * `optional` / `NullOr` wrappers via {@link fieldColumns});
176
+ * - `fromMeta` takes an already-projected metadata map, for a projection this
177
+ * entry knows nothing about (the `./jsonapi/derive` entry is one).
178
+ */
179
+ export const makeDeriveColumns = <B extends ColumnBuilders>(
180
+ builders: B,
181
+ options: DeriveOptions = {},
182
+ ): DeriveColumns<B> => {
183
+ const buildColumn = (meta: DbColumn): ColumnBuilderBase => {
184
+ const build = builders[meta.pg];
185
+ if (build === undefined) {
186
+ throw new DerivationError({
187
+ message: `no builder registered for column kind '${meta.pg}'`,
188
+ });
189
+ }
190
+ // SAFETY: the registry widens each builder's return to `ColumnBuilderBase`,
191
+ // hiding the fluent `.notNull()` / `.default()` / `.references()` methods;
192
+ // the runtime builders are the real Drizzle ones, which carry them.
193
+ let column = build(meta.column) as FluentColumn;
194
+ if (meta.notNull) column = column.notNull();
195
+ if (meta.default !== undefined) column = column.default(meta.default);
196
+ if (meta.references !== undefined) {
197
+ if (options.resolveReference === undefined) {
198
+ throw new DerivationError({
199
+ message: `column '${meta.column}' declares a reference but no resolveReference hook was provided`,
200
+ });
201
+ }
202
+ const target = options.resolveReference(meta.references);
203
+ column = column.references(target, {
204
+ onDelete: meta.references.onDelete,
205
+ onUpdate: meta.references.onUpdate,
206
+ });
207
+ }
208
+ return column;
209
+ };
210
+ const buildColumns = (metas: Record<string, DbColumn>): Record<string, ColumnBuilderBase> =>
211
+ R.fromEntries(
212
+ Arr.map(R.toEntries(metas), ([name, meta]) => [name, buildColumn(meta)] as const),
213
+ );
214
+
215
+ return {
216
+ fromMeta: <Meta extends Record<string, DbColumn>>(metas: Meta): DerivedColumns<Meta, B> =>
217
+ // SAFETY: `buildColumns` maps every key of `metas` to the builder its
218
+ // `pg` kind selects, with `.notNull()` applied exactly when `notNull`
219
+ // is true — the per-entry computation `DerivedColumns` describes.
220
+ buildColumns(metas) as DerivedColumns<Meta, B>,
221
+ fromFields: <F extends Schema.Struct.Fields>(
222
+ fields: F,
223
+ ): DerivedColumns<FieldColumnsMetaOf<F>, B> =>
224
+ // SAFETY: `fieldColumns` projects exactly the annotated fields
225
+ // `FieldColumnsMetaOf` keys, and `buildColumns` maps each as in `fromMeta`.
226
+ buildColumns(fieldColumns(fields)) as DerivedColumns<FieldColumnsMetaOf<F>, B>,
227
+ };
228
+ };
229
+
230
+ // ── CHECK-constraint derivation ───────────────────────────────────────
231
+
232
+ /** A number rendered inline into the CHECK expression (never a bind parameter). */
233
+ const literal = (value: number): SQL => sql.raw(String(value));
234
+
235
+ /**
236
+ * The canonical constraint-name suffix a set of bounds earns: `range` when
237
+ * bounded on both sides; `non_negative` for the sole inclusive-minimum-of-`0`
238
+ * case (the established naming for count/duration columns); `min` / `max` for
239
+ * the remaining one-sided bounds.
240
+ */
241
+ const boundSuffix = (bounds: ColumnBounds): string => {
242
+ const hasMin = bounds.minimum !== undefined || bounds.exclusiveMinimum !== undefined;
243
+ const hasMax = bounds.maximum !== undefined || bounds.exclusiveMaximum !== undefined;
244
+ if (hasMin && hasMax) return "range";
245
+ if (!hasMin) return "max";
246
+ return bounds.minimum === 0 ? "non_negative" : "min";
247
+ };
248
+
249
+ /** The per-side comparisons for a bound set (exclusive bounds win their side). */
250
+ const comparisonParts = (column: ExtraConfigColumn, bounds: ColumnBounds): SQL[] => {
251
+ const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = bounds;
252
+ const parts: SQL[] = [];
253
+ if (exclusiveMinimum !== undefined) parts.push(sql`${column} > ${literal(exclusiveMinimum)}`);
254
+ else if (minimum !== undefined) parts.push(sql`${column} >= ${literal(minimum)}`);
255
+ if (exclusiveMaximum !== undefined) parts.push(sql`${column} < ${literal(exclusiveMaximum)}`);
256
+ else if (maximum !== undefined) parts.push(sql`${column} <= ${literal(maximum)}`);
257
+ return parts;
258
+ };
259
+
260
+ /**
261
+ * The bound expression for one column (without the nullability guard): a
262
+ * both-sides-inclusive bound renders as `col BETWEEN min AND max` — matching
263
+ * the hand-authored year-range constraints in the consuming applications — and
264
+ * anything else as the {@link comparisonParts} joined with `AND`.
265
+ */
266
+ const boundExpression = (column: ExtraConfigColumn, bounds: ColumnBounds): SQL =>
267
+ bounds.minimum !== undefined && bounds.maximum !== undefined
268
+ ? sql`${column} BETWEEN ${literal(bounds.minimum)} AND ${literal(bounds.maximum)}`
269
+ : sql.join(comparisonParts(column, bounds), sql` AND `);
270
+
271
+ /**
272
+ * Derive the SQL `CHECK` constraints an already-projected bounds map implies —
273
+ * the third leg of annotation-driven columns, after the column itself and the
274
+ * read-view selection. Emits one Drizzle `check()` per bounded column, named
275
+ * `${tableName}_${column}_${suffix}` (see {@link boundSuffix} for the canonical
276
+ * suffixes) unless the column annotation pins a `checkName`. A nullable
277
+ * column's expression is guarded with `col IS NULL OR …`, matching the
278
+ * hand-authored precedents.
279
+ *
280
+ * Returns a callback shaped for a table factory's `extraConstraints` seam: it
281
+ * receives the table's columns keyed by **attribute** name (not SQL column
282
+ * name). A bounded attribute whose column is absent from the table is a
283
+ * configuration error ({@link DerivationError}) — bounds on a column the table
284
+ * doesn't carry cannot be enforced.
285
+ */
286
+ export const deriveCheckConstraintsFrom =
287
+ (bounds: Record<string, ColumnBounds>, tableName: string) =>
288
+ (table: Record<string, ExtraConfigColumn>): PgTableExtraConfigValue[] =>
289
+ Arr.map(R.toEntries(bounds), ([attribute, columnBounds]) => {
290
+ const column = table[attribute];
291
+ if (column === undefined) {
292
+ throw new DerivationError({
293
+ message:
294
+ `attribute '${attribute}' declares numeric bounds but ` +
295
+ `table '${tableName}' has no column for it`,
296
+ });
297
+ }
298
+ const expression = boundExpression(column, columnBounds);
299
+ const guarded = columnBounds.column.notNull
300
+ ? expression
301
+ : sql`${column} IS NULL OR ${expression}`;
302
+ const name =
303
+ columnBounds.column.checkName ??
304
+ `${tableName}_${columnBounds.column.column}_${boundSuffix(columnBounds)}`;
305
+ return check(name, guarded);
306
+ });
307
+
308
+ /**
309
+ * {@link deriveCheckConstraintsFrom} over a `Schema.Struct`'s fields: the
310
+ * bounds a model's `Schema.isGreaterThanOrEqualTo` / `isBetween` checks declare
311
+ * ({@link fieldColumnBounds}) become the table's CHECK constraints. One
312
+ * declaration drives wire validation and the database constraint.
313
+ *
314
+ * The projection is deferred to the returned callback, so this composes inside
315
+ * a `pgTable(...)` call whose fields are still being assembled.
316
+ */
317
+ export const deriveFieldCheckConstraints =
318
+ (fields: Schema.Struct.Fields, tableName: string) =>
319
+ (table: Record<string, ExtraConfigColumn>): PgTableExtraConfigValue[] =>
320
+ deriveCheckConstraintsFrom(fieldColumnBounds(fields), tableName)(table);