@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
@@ -0,0 +1,60 @@
1
+ import { h as DerivationError } from "./src-CBeuhy5b.js";
2
+ import "./derive.js";
3
+ import * as Option from "effect/Option";
4
+ import { getColumns, getTableName } from "drizzle-orm";
5
+ import * as MutableHashMap from "effect/MutableHashMap";
6
+ // src/references.ts
7
+ /**
8
+ * @copyright Thomas Foster
9
+ *
10
+ * **FK-reference resolution for annotation-driven columns.**
11
+ *
12
+ * The derivation ({@link ./derive.ts}) reads a column's `references`
13
+ * annotation ({@link DbColumnReference}) as a Drizzle-free **name** pair
14
+ * (`{ table, column }`) and hands it to a `resolveReference` hook that returns
15
+ * the Drizzle column thunk `.references()` needs. This module is one
16
+ * implementation of that hook.
17
+ *
18
+ * Resolution is by **table name**, against a small registry FK-target tables
19
+ * opt into via {@link ReferenceRegistry.register}. Only a table that is
20
+ * actually pointed at by a derived FK column need register. The registry
21
+ * sidesteps the import cycle a static `import { targetTable }` would create
22
+ * (the deriver is imported *by* the table modules): {@link
23
+ * ReferenceRegistry.resolve} returns a **lazy** thunk, and `.references()`
24
+ * invokes it only when the FK is materialised (migration generation / query
25
+ * build) — long after every table module, and its `register` call, has loaded.
26
+ * A reference to an unregistered table fails loudly at that point rather than
27
+ * silently emitting no constraint.
28
+ *
29
+ * **A factory, not a singleton.** The consuming applications each carried one
30
+ * module-scope registry. A library cannot: two independent schemas in one
31
+ * process (an app and its test fixtures, two tenants' table sets) would share
32
+ * one name space and resolve each other's tables. {@link makeReferenceRegistry}
33
+ * hands each derivation its own; a consumer that wants exactly one exports the
34
+ * instance it made.
35
+ *
36
+ * @module references
37
+ */
38
+ /**
39
+ * Build a fresh {@link ReferenceRegistry}. Each call owns its own name → table
40
+ * map, so independent schemas never resolve into each other.
41
+ */
42
+ const makeReferenceRegistry = () => {
43
+ /** Name → table registry of FK-reference targets, populated at module load. */
44
+ const targets = MutableHashMap.empty();
45
+ return {
46
+ register: (table) => {
47
+ MutableHashMap.set(targets, getTableName(table), table);
48
+ },
49
+ resolve: (reference) => () => {
50
+ const table = MutableHashMap.get(targets, reference.table);
51
+ if (Option.isNone(table)) throw new DerivationError({ message: `no FK-reference target registered for table '${reference.table}' — call register(${reference.table}) in its table module` });
52
+ const column = getColumns(table.value)[reference.column];
53
+ if (column === void 0) throw new DerivationError({ message: `table '${reference.table}' has no column '${reference.column}'` });
54
+ return column;
55
+ }
56
+ };
57
+ };
58
+ export { makeReferenceRegistry };
59
+
60
+ //# sourceMappingURL=references.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"references.js","names":[],"sources":["../src/references.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,MAAa,8BAAiD;;CAE5D,MAAM,UAAU,eAAe,MAAuB;CAEtD,OAAO;EACL,WAAW,UAAyB;GAClC,eAAe,IAAI,SAAS,aAAa,KAAK,GAAG,KAAK;EACxD;EACA,UAAU,oBAAoD;GAC5D,MAAM,QAAQ,eAAe,IAAI,SAAS,UAAU,KAAK;GACzD,IAAI,OAAO,OAAO,KAAK,GACrB,MAAM,IAAI,gBAAgB,EACxB,SACE,gDAAgD,UAAU,MAAM,oBAC7C,UAAU,MAAM,uBACvC,CAAC;GAIH,MAAM,SAAU,WAAW,MAAM,KAAK,CAAC,CAAiC,UAAU;GAClF,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,gBAAgB,EACxB,SAAS,UAAU,UAAU,MAAM,mBAAmB,UAAU,OAAO,GACzE,CAAC;GAEH,OAAO;EACT;CACF;AACF"}
package/dist/sort.d.ts ADDED
@@ -0,0 +1,129 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { SQL, Table, View } from "drizzle-orm";
3
+ import { PgColumn } from "drizzle-orm/pg-core";
4
+ import * as Effect from "effect/Effect";
5
+ /**
6
+ * One decoded `?sort=` key: a public field name and its direction. The
7
+ * structural shape every JSON:API sort decoder produces (`-name` →
8
+ * `{ field: "name", direction: "desc" }`); nothing here depends on which
9
+ * decoder that is.
10
+ */
11
+ export interface SortKey {
12
+ readonly field: string;
13
+ readonly direction: "asc" | "desc";
14
+ }
15
+ /**
16
+ * An `ORDER BY` term: a column reference or a SQL expression (including a
17
+ * `SQL.Aliased` view column) — exactly what a Postgres select's `.orderBy()`
18
+ * spreads, and what Drizzle's `asc` / `desc` take.
19
+ */
20
+ export type OrderTerm = PgColumn | SQL | SQL.Aliased;
21
+ /** Public sort-field name → the `ORDER BY` term it resolves to. */
22
+ export type SortableColumns = Readonly<Record<string, OrderTerm>>;
23
+ /**
24
+ * A relation {@link resolveSortOrder} can sort: a Drizzle table or view whose
25
+ * selection carries the `id` column the total-order tiebreak closes on.
26
+ */
27
+ export type SortableRelation = (Table | View) & {
28
+ readonly id: PgColumn;
29
+ };
30
+ declare const UnsupportedSortError_base: Schema.Class<UnsupportedSortError, Schema.TaggedStruct<"UnsupportedSortError", {
31
+ readonly field: Schema.String;
32
+ readonly allowed: Schema.$Array<Schema.String>;
33
+ }>, import("effect/Cause").YieldableError>;
34
+ /**
35
+ * A caller asked to sort by a field the resource does not expose.
36
+ *
37
+ * `allowed` is carried so the error document can tell the caller what it
38
+ * *could* have asked for — the discovery mechanism for a set that is derived
39
+ * rather than published in the endpoint schema. `code` is the stable
40
+ * machine-readable name (`unsupported.sort-field`, the sort-axis counterpart
41
+ * of an include walk's `unsupported.include-path`); a JSON:API boundary maps
42
+ * the error to a 400 with `source: { parameter: "sort" }`.
43
+ */
44
+ export declare class UnsupportedSortError extends UnsupportedSortError_base {
45
+ /** The stable machine-readable error code. */
46
+ get code(): "unsupported.sort-field";
47
+ get message(): string;
48
+ }
49
+ /**
50
+ * How {@link sortableColumnsFor} names the version-chain columns of a view.
51
+ * Every member defaults to the convention `./versioned-chain`'s
52
+ * `makeCurrentVersionView` documents (an `action` soft-delete gate, the
53
+ * current version's `timestamp`, a birth-row `createdAt`).
54
+ */
55
+ export interface SortableColumnsOptions {
56
+ /** Column keys never published as sort fields. Default `["action", "timestamp"]`. */
57
+ readonly internal?: ReadonlyArray<string> | undefined;
58
+ /**
59
+ * Aliased expressions (`SQL.Aliased`, which carry no `dataType`) admitted by
60
+ * name. Default `["createdAt"]`.
61
+ */
62
+ readonly aliased?: ReadonlyArray<string> | undefined;
63
+ /**
64
+ * Public name → the view key it publishes. Applied after `internal`, so an
65
+ * internal key can still be published under its resource-facing name.
66
+ * Default `{ updatedAt: "timestamp" }`.
67
+ */
68
+ readonly renamed?: Readonly<Record<string, string>> | undefined;
69
+ }
70
+ /**
71
+ * The sortable field map for a view (or table) — see the module doc for what
72
+ * lands in it and why.
73
+ */
74
+ export declare const sortableColumnsFor: (relation: Table | View, options?: SortableColumnsOptions) => SortableColumns;
75
+ /** The options {@link resolveSortOrder} takes. */
76
+ export interface ResolveSortOrderOptions {
77
+ /** The relation sorted — the source of both the allow-list and the `id` tiebreak. */
78
+ readonly view: SortableRelation;
79
+ /** The caller's decoded sort keys; absent or empty when they asked for no order. */
80
+ readonly sort?: ReadonlyArray<SortKey> | undefined;
81
+ /**
82
+ * The resource's intrinsic display order, used when the caller supplied no
83
+ * `sort`. Need not be total: the `id` tiebreak is appended unless its last
84
+ * term already *is* the relation's `id` column.
85
+ */
86
+ readonly fallback?: ReadonlyArray<OrderTerm> | undefined;
87
+ /**
88
+ * Direction of the `id` tiebreak appended to a non-total `fallback`.
89
+ * Defaults to the bare (ascending) column. A newest-first fallback passes
90
+ * `"desc"` so the tiebreak mirrors it rather than fighting it.
91
+ */
92
+ readonly tiebreak?: "asc" | "desc" | undefined;
93
+ /** Overrides the derived allow-list for a resource whose sortable set the view can't express. */
94
+ readonly sortable?: SortableColumns | undefined;
95
+ }
96
+ /**
97
+ * Resolve a list request's ordering: the caller's `?sort=` when present, the
98
+ * resource's own `fallback` order otherwise, always closed with an `id`
99
+ * tiebreak so the page window is total-ordered.
100
+ *
101
+ * Fails with {@link UnsupportedSortError} when `sort` names a field outside
102
+ * the allow-list. Every field is checked before any is mapped, so a
103
+ * partially-valid `?sort=` is rejected rather than silently half-applied.
104
+ * The allow-list check is an own-property test, so `constructor` or
105
+ * `toString` are never sort fields.
106
+ */
107
+ export declare const resolveSortOrder: (options: ResolveSortOrderOptions) => Effect.Effect<ReadonlyArray<OrderTerm>, UnsupportedSortError>;
108
+ /** The options {@link chronologicalOrder} takes. */
109
+ export interface ChronologicalOrderOptions {
110
+ /** The relation sorted; its `timestamp` column is the chronological axis. */
111
+ readonly view: SortableRelation & {
112
+ readonly timestamp: PgColumn;
113
+ };
114
+ /** The caller's decoded sort keys, as for {@link resolveSortOrder}. */
115
+ readonly sort?: ReadonlyArray<SortKey> | undefined;
116
+ /** Chronological direction of the default order. Defaults to newest first. */
117
+ readonly direction?: "asc" | "desc" | undefined;
118
+ /** As for {@link resolveSortOrder}. */
119
+ readonly sortable?: SortableColumns | undefined;
120
+ }
121
+ /**
122
+ * {@link resolveSortOrder} for the commonest default a list has:
123
+ * chronological by the current version's `timestamp`, newest first unless
124
+ * told otherwise, with a tiebreak that mirrors it — one decision, spelled
125
+ * once, so the tiebreak cannot go missing at a call site.
126
+ */
127
+ export declare const chronologicalOrder: (options: ChronologicalOrderOptions) => Effect.Effect<ReadonlyArray<OrderTerm>, UnsupportedSortError>;
128
+ export {};
129
+ //# sourceMappingURL=sort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sort.d.ts","names":[],"sources":["../src/sort.ts"],"mappings":";;;;;;;;;;iBA8EiB;WACN;WACA;;;;;;;YAQC,YAAY,WAAW,MAAM,IAAI;;YAGjC,kBAAkB,SAAS,eAAe;;;;;YAM1C,oBAAoB,QAAQ;WAAmB,IAAI;;;;;;;;;;;;;;;;qBAYlD,6BAA6B;;MAQpC;MAIS;;;;;;;;iBAeE;;WAEN,WAAW;;;;;WAKX,UAAU;;;;;;WAMV,UAAU,SAAS;;;;;;qBAiFjB,qBAAkB,UACnB,QAAQ,MAAI,UACZ,2BACT;;iBAgCc;;WAEN,MAAM;;WAEN,OAAO,cAAc;;;;;;WAMrB,WAAW,cAAc;;;;;;WAMzB;;WAEA,WAAW;;;;;;;;;;;;;qBAgBT,mBAAgB,SAClB,4BACR,OAAO,OAAO,cAAc,YAAY;;iBAoC1B;;WAEN,MAAM;aAA8B,WAAW;;;WAE/C,OAAO,cAAc;;WAErB;;WAEA,WAAW;;;;;;;;qBAWT,qBAAkB,SACpB,8BACR,OAAO,OAAO,cAAc,YAAY"}
package/dist/sort.js ADDED
@@ -0,0 +1,225 @@
1
+ import * as Arr from "effect/Array";
2
+ import * as Predicate from "effect/Predicate";
3
+ import * as R from "effect/Record";
4
+ import * as Schema from "effect/Schema";
5
+ import { SQL, asc, desc, getColumns } from "drizzle-orm";
6
+ import * as Effect from "effect/Effect";
7
+ import * as HashSet from "effect/HashSet";
8
+ // src/sort.ts
9
+ /**
10
+ * @copyright Thomas Foster
11
+ *
12
+ * **The `?sort=` resolution seam: which fields a list may be sorted by, and
13
+ * the `ORDER BY` terms a decoded sort list becomes.**
14
+ *
15
+ * The wire grammar (`?sort=-publishedAt,name`) and its bounds are the
16
+ * request decoder's: by the time anything here runs, a malformed or
17
+ * over-wide `?sort=` has already been refused, and what arrives is a list of
18
+ * {@link SortKey}s. What this module owns is the half that needs to know
19
+ * about columns — **which** fields a view may be sorted by, and what SQL each
20
+ * one becomes.
21
+ *
22
+ * ## The allow-list is derived, not hand-listed
23
+ *
24
+ * {@link sortableColumnsFor} derives a view's sortable set from the view
25
+ * itself, filtered to columns of a scalar, orderable type and published under
26
+ * stable public names:
27
+ *
28
+ * - every column whose Drizzle `dataType` is a string / number / bigint /
29
+ * boolean / timestamp, under the column's own key;
30
+ * - the {@link SortableColumnsOptions.aliased | aliased} expressions it is
31
+ * told to admit by name (a `SQL.Aliased` carries no `dataType`) —
32
+ * by default `createdAt`, the birth-row timestamp a function `merge` of
33
+ * `./versioned-chain`'s `makeCurrentVersionView` merges in;
34
+ * - the {@link SortableColumnsOptions.renamed | renamed} columns under their
35
+ * public names — by default the current version's `timestamp` as
36
+ * `updatedAt`.
37
+ *
38
+ * Everything else is refused with {@link UnsupportedSortError}: a field that
39
+ * does not exist, a column whose type has no useful order (a document
40
+ * column, an embedding), and the version-chain internals
41
+ * ({@link SortableColumnsOptions.internal}, by default `action` and the raw
42
+ * `timestamp` key) that are not part of any resource's vocabulary. Deriving
43
+ * is what makes the surface uniform: a resource gains its sort surface by
44
+ * existing, and a new attribute is sortable the moment its column lands in
45
+ * the view. A resource whose sortable set the view cannot express passes an
46
+ * explicit `sortable` map instead.
47
+ *
48
+ * ## Cost
49
+ *
50
+ * Sorting here is bounded but not necessarily index-backed: a current-version
51
+ * view is a projection over an append-only chain, so the database
52
+ * materialises the current set and sorts it whatever the `ORDER BY` is. A
53
+ * client `?sort=` changes *which* column is sorted, not the cost class of the
54
+ * query, and the page stays `LIMIT`-bounded exactly as before. If a view
55
+ * grows past the point where sorting its whole current set is cheap, the fix
56
+ * is an indexed ordering column, not a narrower allow-list.
57
+ *
58
+ * ## Total order (website #972, #1231)
59
+ *
60
+ * `LIMIT` / `OFFSET` over a non-total order lets a row repeat across pages or
61
+ * fall through the gap between them. Every order {@link resolveSortOrder}
62
+ * returns therefore ends in the view's `id`, mirroring the direction of the
63
+ * last sort key, unless the caller already sorted by `id`.
64
+ *
65
+ * Ported from the website and gardeneria applications'
66
+ * `apps/api/src/operations/sort.ts`.
67
+ *
68
+ * @module sort
69
+ */
70
+ /**
71
+ * A caller asked to sort by a field the resource does not expose.
72
+ *
73
+ * `allowed` is carried so the error document can tell the caller what it
74
+ * *could* have asked for — the discovery mechanism for a set that is derived
75
+ * rather than published in the endpoint schema. `code` is the stable
76
+ * machine-readable name (`unsupported.sort-field`, the sort-axis counterpart
77
+ * of an include walk's `unsupported.include-path`); a JSON:API boundary maps
78
+ * the error to a 400 with `source: { parameter: "sort" }`.
79
+ */
80
+ var UnsupportedSortError = class extends Schema.TaggedError()("UnsupportedSortError", {
81
+ field: Schema.String,
82
+ allowed: Schema.Array(Schema.String)
83
+ }) {
84
+ /** The stable machine-readable error code. */
85
+ get code() {
86
+ return "unsupported.sort-field";
87
+ }
88
+ get message() {
89
+ return `unsupported.sort-field: ${this.field}. Sortable fields: ${Arr.match(this.allowed, {
90
+ onEmpty: () => "(none)",
91
+ onNonEmpty: (allowed) => allowed.join(", ")
92
+ })}`;
93
+ }
94
+ };
95
+ const DEFAULT_INTERNAL = ["action", "timestamp"];
96
+ const DEFAULT_ALIASED = ["createdAt"];
97
+ const DEFAULT_RENAMED = { updatedAt: "timestamp" };
98
+ /**
99
+ * Drizzle `dataType` base tokens that order usefully. The descriptor is
100
+ * space-separated with the base type first (`string`, `string uuid`,
101
+ * `number int32`, `object date`, …); timestamps' base token is the useless
102
+ * `object`, so they are matched in full. What this excludes is the point:
103
+ * `custom` columns and `array vector` embeddings have no meaningful scalar
104
+ * order.
105
+ */
106
+ const SORTABLE_BASE_TYPES = HashSet.make("string", "number", "bigint", "boolean");
107
+ const isSortableColumn = (term) => {
108
+ if (!Predicate.hasProperty(term, "dataType") || !Predicate.isString(term.dataType)) return false;
109
+ if (term.dataType === "object date") return true;
110
+ return HashSet.has(SORTABLE_BASE_TYPES, term.dataType.split(" ")[0] ?? "");
111
+ };
112
+ /**
113
+ * Derived maps, memoised per relation and options object. A view is a
114
+ * module-scope value and the derivation walks every column, so recomputing
115
+ * it per list request would be pure waste. Only the default-options
116
+ * derivation is memoised: an options object is usually a fresh literal, and
117
+ * keying on it would only grow the cache.
118
+ */
119
+ const derived = /* @__PURE__ */ new WeakMap();
120
+ const resolveNames = (options = {}) => ({
121
+ internal: HashSet.fromIterable(options.internal ?? DEFAULT_INTERNAL),
122
+ aliased: HashSet.fromIterable(options.aliased ?? DEFAULT_ALIASED),
123
+ renamed: options.renamed ?? DEFAULT_RENAMED
124
+ });
125
+ /**
126
+ * Whether one view entry is published under its own key. A `SQL.Aliased`
127
+ * view column carries no `dataType`, so it is admitted by name; Drizzle
128
+ * renders it in `ORDER BY` as `"<view>"."<alias>"`, which is exactly right in
129
+ * the outer query.
130
+ */
131
+ const admits = (names, key, term) => {
132
+ if (HashSet.has(names.internal, key)) return false;
133
+ if (term instanceof SQL.Aliased) return HashSet.has(names.aliased, key);
134
+ return isSortableColumn(term);
135
+ };
136
+ const deriveSortable = (relation, names) => {
137
+ const columns = getColumns(relation);
138
+ const published = Arr.filter(R.toEntries(columns), ([key, term]) => admits(names, key, term));
139
+ const renamed = Arr.flatMap(R.toEntries(names.renamed), ([name, key]) => {
140
+ const term = columns[key];
141
+ return term === void 0 ? [] : [[name, term]];
142
+ });
143
+ return R.fromEntries([...published, ...renamed]);
144
+ };
145
+ /**
146
+ * The sortable field map for a view (or table) — see the module doc for what
147
+ * lands in it and why.
148
+ */
149
+ const sortableColumnsFor = (relation, options) => {
150
+ if (options !== void 0) return deriveSortable(relation, resolveNames(options));
151
+ const cached = derived.get(relation);
152
+ if (cached !== void 0) return cached;
153
+ const map = deriveSortable(relation, resolveNames());
154
+ derived.set(relation, map);
155
+ return map;
156
+ };
157
+ /** Apply a direction to one `ORDER BY` term. */
158
+ const directed = (term, direction) => direction === "desc" ? desc(term) : asc(term);
159
+ /**
160
+ * Whether an ordering already ends in the relation's bare `id` column, and
161
+ * so needs no tiebreak appended.
162
+ *
163
+ * Compared by **column name**, not by reference: Drizzle hands back a fresh
164
+ * alias proxy on every view-column access, so `view.id === view.id` is
165
+ * `false` and an identity check silently never matches. A directed term
166
+ * (`desc(view.id)`) is a `SQL`, not a column, and is deliberately not
167
+ * matched: appending an ascending `id` after a descending one would fight it,
168
+ * so a caller spelling the tiebreak that way passes `tiebreak` instead.
169
+ */
170
+ const endsInIdColumn = (order, idColumn) => {
171
+ const last = order.at(-1);
172
+ if (last === void 0 || last instanceof SQL || last instanceof SQL.Aliased) return false;
173
+ return last.name === idColumn.name;
174
+ };
175
+ /**
176
+ * Resolve a list request's ordering: the caller's `?sort=` when present, the
177
+ * resource's own `fallback` order otherwise, always closed with an `id`
178
+ * tiebreak so the page window is total-ordered.
179
+ *
180
+ * Fails with {@link UnsupportedSortError} when `sort` names a field outside
181
+ * the allow-list. Every field is checked before any is mapped, so a
182
+ * partially-valid `?sort=` is rejected rather than silently half-applied.
183
+ * The allow-list check is an own-property test, so `constructor` or
184
+ * `toString` are never sort fields.
185
+ */
186
+ const resolveSortOrder = (options) => Effect.suspend(() => {
187
+ const idColumn = options.view.id;
188
+ return Arr.match(options.sort ?? [], {
189
+ onEmpty: () => {
190
+ const fallback = options.fallback ?? [];
191
+ if (endsInIdColumn(fallback, idColumn)) return Effect.succeed(fallback);
192
+ return Effect.succeed([...fallback, options.tiebreak === void 0 ? idColumn : directed(idColumn, options.tiebreak)]);
193
+ },
194
+ onNonEmpty: (sort) => {
195
+ const sortable = options.sortable ?? sortableColumnsFor(options.view);
196
+ const unsupported = sort.find(({ field }) => !Object.hasOwn(sortable, field));
197
+ if (unsupported !== void 0) return Effect.fail(new UnsupportedSortError({
198
+ field: unsupported.field,
199
+ allowed: R.keys(sortable).toSorted()
200
+ }));
201
+ const terms = sort.map(({ field, direction }) => directed(sortable[field], direction));
202
+ if (sort.some(({ field }) => field === "id")) return Effect.succeed(terms);
203
+ return Effect.succeed([...terms, directed(idColumn, Arr.lastNonEmpty(sort).direction)]);
204
+ }
205
+ });
206
+ });
207
+ /**
208
+ * {@link resolveSortOrder} for the commonest default a list has:
209
+ * chronological by the current version's `timestamp`, newest first unless
210
+ * told otherwise, with a tiebreak that mirrors it — one decision, spelled
211
+ * once, so the tiebreak cannot go missing at a call site.
212
+ */
213
+ const chronologicalOrder = (options) => {
214
+ const direction = options.direction ?? "desc";
215
+ return resolveSortOrder({
216
+ view: options.view,
217
+ sort: options.sort,
218
+ fallback: [directed(options.view.timestamp, direction)],
219
+ tiebreak: direction,
220
+ sortable: options.sortable
221
+ });
222
+ };
223
+ export { UnsupportedSortError, chronologicalOrder, resolveSortOrder, sortableColumnsFor };
224
+
225
+ //# sourceMappingURL=sort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sort.js","names":[],"sources":["../src/sort.ts"],"mappings":"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"}