@orthacms/utils-server 0.0.0-reserve.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +10 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +24 -0
  6. package/dist/lib/clamp-int.d.ts +8 -0
  7. package/dist/lib/clamp-int.d.ts.map +1 -0
  8. package/dist/lib/clamp-int.js +15 -0
  9. package/dist/lib/filters/filter-exceptions.d.ts +92 -0
  10. package/dist/lib/filters/filter-exceptions.d.ts.map +1 -0
  11. package/dist/lib/filters/filter-exceptions.js +117 -0
  12. package/dist/lib/filters/negation.d.ts +33 -0
  13. package/dist/lib/filters/negation.d.ts.map +1 -0
  14. package/dist/lib/filters/negation.js +59 -0
  15. package/dist/lib/filters/operator-support.d.ts +25 -0
  16. package/dist/lib/filters/operator-support.d.ts.map +1 -0
  17. package/dist/lib/filters/operator-support.js +88 -0
  18. package/dist/lib/filters/own-property.d.ts +28 -0
  19. package/dist/lib/filters/own-property.d.ts.map +1 -0
  20. package/dist/lib/filters/own-property.js +34 -0
  21. package/dist/lib/filters/parse-filter-tree.d.ts +17 -0
  22. package/dist/lib/filters/parse-filter-tree.d.ts.map +1 -0
  23. package/dist/lib/filters/parse-filter-tree.js +107 -0
  24. package/dist/lib/filters/relation-exists.d.ts +35 -0
  25. package/dist/lib/filters/relation-exists.d.ts.map +1 -0
  26. package/dist/lib/filters/relation-exists.js +119 -0
  27. package/dist/lib/filters/resolve-leaf.d.ts +8 -0
  28. package/dist/lib/filters/resolve-leaf.d.ts.map +1 -0
  29. package/dist/lib/filters/resolve-leaf.js +170 -0
  30. package/dist/lib/filters/scalar-op.d.ts +14 -0
  31. package/dist/lib/filters/scalar-op.d.ts.map +1 -0
  32. package/dist/lib/filters/scalar-op.js +66 -0
  33. package/dist/lib/filters/table-helpers.d.ts +67 -0
  34. package/dist/lib/filters/table-helpers.d.ts.map +1 -0
  35. package/dist/lib/filters/table-helpers.js +68 -0
  36. package/dist/lib/filters/tree-to-drizzle.d.ts +35 -0
  37. package/dist/lib/filters/tree-to-drizzle.d.ts.map +1 -0
  38. package/dist/lib/filters/tree-to-drizzle.js +76 -0
  39. package/dist/lib/filters/types.d.ts +217 -0
  40. package/dist/lib/filters/types.d.ts.map +1 -0
  41. package/dist/lib/filters/types.js +35 -0
  42. package/dist/lib/pg-errors.d.ts +43 -0
  43. package/dist/lib/pg-errors.d.ts.map +1 -0
  44. package/dist/lib/pg-errors.js +82 -0
  45. package/package.json +35 -0
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.columnOf = columnOf;
4
+ exports.rebind = rebind;
5
+ exports.primaryKey = primaryKey;
6
+ const drizzle_orm_1 = require("drizzle-orm");
7
+ const filter_exceptions_1 = require("./filter-exceptions");
8
+ /* eslint-enable @typescript-eslint/no-explicit-any */
9
+ /**
10
+ * Resolve a column on a table by name, or throw if absent.
11
+ *
12
+ * The lookup is `Object.hasOwn`-guarded: drizzle's column map is a plain
13
+ * object, so `cols['constructor']` would otherwise return `Object` itself and
14
+ * this function would hand a `Function` to drizzle as a `Column`. The parser's
15
+ * whitelist is the primary guard, but this is the site where an escaped name
16
+ * turns into malformed SQL rather than an error, so it guards itself too.
17
+ */
18
+ function columnOf(table, name) {
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const cols = (0, drizzle_orm_1.getTableColumns)(table);
21
+ const col = Object.hasOwn(cols, name) ? cols[name] : undefined;
22
+ if (!col)
23
+ throw new filter_exceptions_1.FilterSchemaException(`column "${name}" not on table`);
24
+ return col;
25
+ }
26
+ /**
27
+ * Re-resolve a prebuilt column against the table the translator is actually
28
+ * querying.
29
+ *
30
+ * A `RelationSchema`'s parent-side columns (`fk` on a `many-to-one` /
31
+ * `self-referential`, and every `parentKey`) are built once, against the
32
+ * **physical** parent table. That is wrong the moment the parent is an
33
+ * *alias* — which happens for every relation nested under a
34
+ * `self-referential` hop, whose subquery is `FROM t AS qb_x`. An unaliased
35
+ * `t.col` inside that subquery is still resolvable from the OUTERMOST
36
+ * query's `FROM t`, so Postgres silently binds the correlation to the root
37
+ * row instead of the aliased one: `parent.author.name` would filter on the
38
+ * ROOT row's author, and `parent.parent.name` would collapse to
39
+ * `parent.name`. Valid SQL, wrong rows, no error.
40
+ *
41
+ * Rebinding by the column's own DB name against the queried table fixes the
42
+ * correlation. It is a no-op when the parent is the physical table (the
43
+ * lookup finds the identical column), so unaliased paths are unaffected.
44
+ * Only ever call this for columns that live on the parent **by
45
+ * construction** — never for a target-side `fk` (`one-to-many`), where the
46
+ * name could collide with an unrelated parent column.
47
+ */
48
+ function rebind(column, table) {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const cols = (0, drizzle_orm_1.getTableColumns)(table);
51
+ for (const candidate of Object.values(cols)) {
52
+ if (candidate.name === column.name)
53
+ return candidate;
54
+ }
55
+ return column;
56
+ }
57
+ /**
58
+ * Resolve a table's primary key (defaults to an `id` column).
59
+ * Callers can override with an explicit `parentKey`/`targetKey` in the
60
+ * relation schema when the table's primary key is named differently.
61
+ */
62
+ function primaryKey(table) {
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ const cols = (0, drizzle_orm_1.getTableColumns)(table);
65
+ if (Object.hasOwn(cols, 'id'))
66
+ return cols['id'];
67
+ throw new filter_exceptions_1.FilterSchemaException('table has no `id` column — declare parentKey/targetKey explicitly');
68
+ }
@@ -0,0 +1,35 @@
1
+ import { type SQL } from 'drizzle-orm';
2
+ import { type DbLike, type TableLike } from './table-helpers';
3
+ import type { FilterSchema, ParsedNode, ParsedRule } from './types';
4
+ export type { DbLike, TableLike } from './table-helpers';
5
+ /**
6
+ * Hook a host can supply to handle filter rules whose field is in
7
+ * {@link FilterSchema.extensionFields}. Returns a Drizzle SQL fragment
8
+ * that gets spliced into the WHERE tree at the leaf's position, so
9
+ * extension rules compose with native ones inside AND/OR groups.
10
+ *
11
+ * The translator awaits each call sequentially during the walk; if
12
+ * an extension performs a DB roundtrip its latency adds linearly per
13
+ * leaf — extensions that need expensive lookups should prepare data
14
+ * once per request and capture it in the closure.
15
+ */
16
+ export type FilterExtensionResolver = (rule: ParsedRule) => Promise<SQL>;
17
+ /** Optional translator hooks — currently just the extension resolver. */
18
+ export interface ApplyFilterTreeOptions {
19
+ /** Called for any leaf whose `path[0]` is in `schema.extensionFields`. */
20
+ resolveExtension?: FilterExtensionResolver;
21
+ }
22
+ /**
23
+ * Translate a parsed filter tree into a single Drizzle SQL fragment.
24
+ * Returns `undefined` when the tree is empty (or every group collapses
25
+ * to nothing) so callers can skip the WHERE clause without a special
26
+ * case. Group nodes emit `and(...)` / `or(...)`; rule nodes share the
27
+ * same scalar / relation translation.
28
+ *
29
+ * Async because extension resolvers may be async (e.g. a role filter
30
+ * runs a tiny `SELECT slug FROM roles` to validate enum membership).
31
+ * Non-extension trees stay synchronous in spirit — every native branch
32
+ * resolves immediately, no awaits hit the wire.
33
+ */
34
+ export declare function applyFilterTree(tree: ParsedNode | null | undefined, schema: FilterSchema, rootTable: TableLike, db: DbLike, options?: ApplyFilterTreeOptions): Promise<SQL | undefined>;
35
+ //# sourceMappingURL=tree-to-drizzle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-to-drizzle.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/tree-to-drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AAMrD,OAAO,EAAY,KAAK,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAEzE,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACnC,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;CAC9C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACjC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,SAAS,EACpB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,sBAA2B,GACrC,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC,CAG1B"}
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyFilterTree = applyFilterTree;
4
+ const drizzle_orm_1 = require("drizzle-orm");
5
+ const filter_exceptions_1 = require("./filter-exceptions");
6
+ const negation_1 = require("./negation");
7
+ const own_property_1 = require("./own-property");
8
+ const relation_exists_1 = require("./relation-exists");
9
+ const scalar_op_1 = require("./scalar-op");
10
+ const table_helpers_1 = require("./table-helpers");
11
+ /**
12
+ * Translate a parsed filter tree into a single Drizzle SQL fragment.
13
+ * Returns `undefined` when the tree is empty (or every group collapses
14
+ * to nothing) so callers can skip the WHERE clause without a special
15
+ * case. Group nodes emit `and(...)` / `or(...)`; rule nodes share the
16
+ * same scalar / relation translation.
17
+ *
18
+ * Async because extension resolvers may be async (e.g. a role filter
19
+ * runs a tiny `SELECT slug FROM roles` to validate enum membership).
20
+ * Non-extension trees stay synchronous in spirit — every native branch
21
+ * resolves immediately, no awaits hit the wire.
22
+ */
23
+ async function applyFilterTree(tree, schema, rootTable, db, options = {}) {
24
+ if (!tree)
25
+ return undefined;
26
+ return walk(tree, schema, rootTable, db, options);
27
+ }
28
+ async function walk(node, schema, parent, db, options) {
29
+ if (node.kind === 'rule') {
30
+ return translateRule(node, schema, parent, db, options);
31
+ }
32
+ const parts = [];
33
+ for (const child of node.children) {
34
+ const piece = await walk(child, schema, parent, db, options);
35
+ if (piece !== undefined)
36
+ parts.push(piece);
37
+ }
38
+ if (parts.length === 0)
39
+ return undefined;
40
+ if (parts.length === 1)
41
+ return parts[0];
42
+ return node.combinator === 'or' ? (0, drizzle_orm_1.or)(...parts) : (0, drizzle_orm_1.and)(...parts);
43
+ }
44
+ async function translateRule(rule, schema, parent, db, options) {
45
+ const head = rule.path[0];
46
+ if (schema.extensionFields?.has(head)) {
47
+ if (!options.resolveExtension) {
48
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, `extension field "${head}" referenced but no resolver provided`, { field: head });
49
+ }
50
+ return options.resolveExtension(rule);
51
+ }
52
+ if (rule.path.length === 1) {
53
+ return (0, scalar_op_1.scalar)((0, table_helpers_1.columnOf)(parent, rule.path[0]), rule.op, rule.value);
54
+ }
55
+ const key = rule.path[0];
56
+ // `Object.hasOwn`-guarded: `relations['valueOf']` on a plain object map
57
+ // returns `Object.prototype.valueOf`, which passes this truthiness check
58
+ // and then falls out of `relationExists`'s `switch (rel.kind)` as
59
+ // `undefined` — the predicate the user asked for is silently dropped and
60
+ // the endpoint answers 200 unfiltered.
61
+ const rel = (0, own_property_1.own)(schema.relations, key);
62
+ if (!rel) {
63
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.UnknownRelation, `unknown relation "${key}"`, { relation: key });
64
+ }
65
+ const leaf = { path: rule.path.slice(1), op: rule.op, value: rule.value };
66
+ // A negating rule on a relation path asks for the ABSENCE of a matching
67
+ // related row, which is `NOT EXISTS(… positive …)` — NOT the naive
68
+ // `EXISTS(… negated …)`, which on a to-many relation asserts the
69
+ // opposite of what the user wrote. The negation wraps the OUTERMOST
70
+ // hop, so a multi-hop path negates the whole chain ("no (author,
71
+ // company) pair matches") rather than just its last segment.
72
+ if ((0, negation_1.isNegatingLeaf)(rule.op, rule.value)) {
73
+ return (0, drizzle_orm_1.not)((0, relation_exists_1.relationExists)(rel, parent, (0, negation_1.positiveLeaf)(leaf), db));
74
+ }
75
+ return (0, relation_exists_1.relationExists)(rel, parent, leaf, db);
76
+ }
@@ -0,0 +1,217 @@
1
+ import type { AnyColumn, SQL, Table } from 'drizzle-orm';
2
+ import type { TableLike } from './table-helpers';
3
+ /**
4
+ * Builds a relation's `scope` predicate — the workspace + soft-delete
5
+ * guard ANDed inside its EXISTS subquery. It is a function, not a
6
+ * prebuilt `SQL`, because the table it must reference is not always the
7
+ * physical target: a `self-referential` relation aliases the target, and
8
+ * the predicate has to bind to that alias, not the outer table of the
9
+ * same name. The translator passes whichever table it actually queries
10
+ * (aliased or not), so the host resolves columns from that — e.g. via
11
+ * `getTableColumns(target)`. Return `undefined` to add nothing.
12
+ */
13
+ export type RelationScope = (target: TableLike) => SQL | undefined;
14
+ /** Named constants for `FilterOperator` — use in switches and comparisons. */
15
+ export declare const FilterOperator: {
16
+ readonly Eq: "eq";
17
+ readonly Ne: "ne";
18
+ readonly Gt: "gt";
19
+ readonly Gte: "gte";
20
+ readonly Lt: "lt";
21
+ readonly Lte: "lte";
22
+ readonly In: "in";
23
+ readonly Nin: "nin";
24
+ readonly Like: "like";
25
+ readonly Ilike: "ilike";
26
+ readonly Nilike: "nilike";
27
+ readonly Null: "null";
28
+ };
29
+ /** Standard REST operator names. Translator maps each to a Drizzle helper. */
30
+ export type FilterOperator = (typeof FilterOperator)[keyof typeof FilterOperator];
31
+ /** Named constants for `ScalarFieldType` — use in switches. */
32
+ export declare const ScalarFieldType: {
33
+ readonly String: "string";
34
+ readonly Number: "number";
35
+ readonly Boolean: "boolean";
36
+ readonly Uuid: "uuid";
37
+ readonly Date: "date";
38
+ readonly Enum: "enum";
39
+ };
40
+ /** How the parser coerces raw URL string values. */
41
+ export type ScalarFieldType = (typeof ScalarFieldType)[keyof typeof ScalarFieldType];
42
+ /** Named constants for `RelationSchema['kind']` — use in switches. */
43
+ export declare const RelationKind: {
44
+ readonly OneToOne: "one-to-one";
45
+ readonly OneToMany: "one-to-many";
46
+ readonly ManyToOne: "many-to-one";
47
+ readonly ManyToMany: "many-to-many";
48
+ readonly SelfReferential: "self-referential";
49
+ };
50
+ /** Relation cardinality discriminants. */
51
+ export type RelationKind = (typeof RelationKind)[keyof typeof RelationKind];
52
+ /**
53
+ * Coercion rules for one column.
54
+ *
55
+ * The declared `type` decides two things, not one: how an incoming string is
56
+ * coerced, and **which operators the field may be asked** (`operator-support.ts`
57
+ * holds the table). A field does not accept every operator — the pattern family
58
+ * (`like`/`ilike`/`nilike`) is text-only, because Postgres defines `~~` for text
59
+ * and nothing else.
60
+ */
61
+ export interface ScalarFieldSchema {
62
+ /** How incoming strings are coerced before hitting Drizzle. */
63
+ type: ScalarFieldType;
64
+ /** Required when `type === 'enum'`. */
65
+ enumValues?: readonly string[];
66
+ }
67
+ /** Columns exposed on one table. */
68
+ export type FieldSchema = Record<string, ScalarFieldSchema>;
69
+ /**
70
+ * Relation descriptor. `kind` discriminates the SQL shape the translator
71
+ * emits (EXISTS, EXISTS + INNER JOIN, aliased self-join).
72
+ */
73
+ export type RelationSchema = {
74
+ /** Target FK references parent — one row (1:1) or many (1:N). */
75
+ kind: 'one-to-one' | 'one-to-many';
76
+ /** Target table containing the FK. */
77
+ table: Table;
78
+ /** Column on `table` referencing parent's primary key. */
79
+ fk: AnyColumn;
80
+ /** Parent primary key — defaults to `parent.id`. */
81
+ parentKey?: AnyColumn;
82
+ /**
83
+ * Extra predicate ANDed inside the EXISTS subquery, over the
84
+ * target table's own columns — e.g. a workspace boundary and a
85
+ * soft-delete guard. Without it a relation filter traverses rows
86
+ * the root query itself excludes (a soft-deleted or foreign
87
+ * target still matches `relation.field`). See {@link RelationScope}.
88
+ */
89
+ scope?: RelationScope;
90
+ fields?: FieldSchema;
91
+ relations?: Record<string, RelationSchema>;
92
+ } | {
93
+ /** Parent FK references target. */
94
+ kind: 'many-to-one';
95
+ /** Target table. */
96
+ table: Table;
97
+ /** Column on the parent table referencing target's primary key. */
98
+ fk: AnyColumn;
99
+ /** Target primary key — defaults to `target.id`. */
100
+ targetKey?: AnyColumn;
101
+ /** Workspace + soft-delete guard. See {@link RelationScope}. */
102
+ scope?: RelationScope;
103
+ fields?: FieldSchema;
104
+ relations?: Record<string, RelationSchema>;
105
+ } | {
106
+ /** Many rows link parent to target through a junction table. */
107
+ kind: 'many-to-many';
108
+ /** Junction (through) table. */
109
+ through: Table;
110
+ /** Junction column referencing parent's primary key. */
111
+ fk: AnyColumn;
112
+ /** Junction column referencing target's primary key. */
113
+ targetFk: AnyColumn;
114
+ /** Target table — required when filtering on target fields. */
115
+ table?: Table;
116
+ parentKey?: AnyColumn;
117
+ targetKey?: AnyColumn;
118
+ /**
119
+ * Workspace + soft-delete guard. Because a scope lives on the
120
+ * target table, it can only be applied through the join to
121
+ * `table` — so when it is present the junction-only fast path
122
+ * (filtering on the target FK alone) must yield to the full join.
123
+ * See {@link RelationScope}.
124
+ */
125
+ scope?: RelationScope;
126
+ fields?: FieldSchema;
127
+ relations?: Record<string, RelationSchema>;
128
+ } | {
129
+ /** Parent references another row in the same table. */
130
+ kind: 'self-referential';
131
+ /** Same physical table as the parent. */
132
+ table: Table;
133
+ /** Column on the parent referencing its own primary key. */
134
+ fk: AnyColumn;
135
+ /**
136
+ * Alias used for the self-joined table inside the subquery. Must
137
+ * be unique per occurrence in the filter tree, not per relation:
138
+ * two rules on the same self-relation would otherwise share a
139
+ * correlation name and collide.
140
+ */
141
+ alias: string;
142
+ /**
143
+ * Workspace + soft-delete guard, applied against the aliased
144
+ * self-join (the translator passes the alias). See
145
+ * {@link RelationScope}.
146
+ */
147
+ scope?: RelationScope;
148
+ fields?: FieldSchema;
149
+ relations?: Record<string, RelationSchema>;
150
+ };
151
+ /** Public filter surface for one endpoint. */
152
+ export interface FilterSchema {
153
+ /** Scalar columns on the root table. */
154
+ fields?: FieldSchema;
155
+ /** Relations traversable from the root table. */
156
+ relations?: Record<string, RelationSchema>;
157
+ /**
158
+ * Field names that should be routed through a host-supplied
159
+ * `resolveExtension` hook instead of the standard scalar / relation
160
+ * translator. Lets a downstream plugin contribute a "virtual"
161
+ * field — e.g. `role` on the user filter — that resolves to a
162
+ * subquery the host stitches into the WHERE tree.
163
+ *
164
+ * Each entry MUST also have a corresponding declaration under
165
+ * {@link fields} so the parser can validate the leaf's value
166
+ * against a {@link ScalarFieldSchema}. The set is consulted only
167
+ * by the translator, not by `parseFilterTree`.
168
+ */
169
+ extensionFields?: ReadonlySet<string>;
170
+ /** Maximum dotted-path depth. Defaults to 3. */
171
+ maxDepth?: number;
172
+ /**
173
+ * Maximum total number of rules + groups in a tree-shaped filter.
174
+ * Defaults to 50. Guards against pathological JSON payloads that
175
+ * would otherwise translate into very large SQL trees.
176
+ */
177
+ maxNodes?: number;
178
+ /**
179
+ * Maximum nesting depth of `and`/`or` groups in a tree-shaped filter.
180
+ * Defaults to 5. Independent of {@link maxDepth} (which limits
181
+ * dotted-path depth on a single rule).
182
+ */
183
+ maxGroupDepth?: number;
184
+ /**
185
+ * Maximum number of elements in a single `in`/`nin` value list.
186
+ * Defaults to 100. Bounds the size of the generated `IN (...)` clause
187
+ * independently of {@link maxNodes} (which counts whole rules, not the
188
+ * elements inside one rule's value list).
189
+ */
190
+ maxInListLength?: number;
191
+ }
192
+ /** Parser output, one node per URL filter entry. */
193
+ export interface ParsedFilter {
194
+ /** Dotted path split into segments, e.g. `['workspaces','name']`. */
195
+ path: string[];
196
+ /** Requested operator. */
197
+ op: FilterOperator;
198
+ /** Coerced to the declared type; array for `in`/`nin`, boolean for `null`. */
199
+ value: unknown;
200
+ }
201
+ /** Tree-shaped parser output — leaf rule, type-tagged for the union. */
202
+ export interface ParsedRule extends ParsedFilter {
203
+ kind: 'rule';
204
+ }
205
+ /** Tree-shaped parser output — group node combining children with AND or OR. */
206
+ export interface ParsedGroup {
207
+ kind: 'group';
208
+ combinator: 'and' | 'or';
209
+ children: ParsedNode[];
210
+ }
211
+ /**
212
+ * Tree-shaped parser output. `parseFilterTree` returns a `ParsedGroup`
213
+ * root (or null when the input is empty); the translator walks this
214
+ * tree directly into Drizzle SQL.
215
+ */
216
+ export type ParsedNode = ParsedRule | ParsedGroup;
217
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RelationKind = exports.ScalarFieldType = exports.FilterOperator = void 0;
4
+ /** Named constants for `FilterOperator` — use in switches and comparisons. */
5
+ exports.FilterOperator = {
6
+ Eq: 'eq',
7
+ Ne: 'ne',
8
+ Gt: 'gt',
9
+ Gte: 'gte',
10
+ Lt: 'lt',
11
+ Lte: 'lte',
12
+ In: 'in',
13
+ Nin: 'nin',
14
+ Like: 'like',
15
+ Ilike: 'ilike',
16
+ Nilike: 'nilike',
17
+ Null: 'null'
18
+ };
19
+ /** Named constants for `ScalarFieldType` — use in switches. */
20
+ exports.ScalarFieldType = {
21
+ String: 'string',
22
+ Number: 'number',
23
+ Boolean: 'boolean',
24
+ Uuid: 'uuid',
25
+ Date: 'date',
26
+ Enum: 'enum'
27
+ };
28
+ /** Named constants for `RelationSchema['kind']` — use in switches. */
29
+ exports.RelationKind = {
30
+ OneToOne: 'one-to-one',
31
+ OneToMany: 'one-to-many',
32
+ ManyToOne: 'many-to-one',
33
+ ManyToMany: 'many-to-many',
34
+ SelfReferential: 'self-referential'
35
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Postgres error introspection shared by services that map constraint
3
+ * violations to clean HTTP errors (e.g. a unique-index race settled by the
4
+ * constraint → 409) instead of leaking a 500.
5
+ */
6
+ /**
7
+ * Whether an error (or its `cause`, where a driver/ORM wraps it) is a
8
+ * Postgres unique violation (`23505`). Drivers surface the SQLSTATE on a
9
+ * `code` property; the walk is defensive because Drizzle has wrapped driver
10
+ * errors differently across versions.
11
+ */
12
+ export declare function isUniqueViolation(error: unknown): boolean;
13
+ /**
14
+ * The **name of the index/constraint** a unique violation (`23505`) tripped, or
15
+ * `undefined` when the error is not one.
16
+ *
17
+ * A table can carry several unique indexes, and "which one" is the difference
18
+ * between two entirely different messages to the caller — a localized content
19
+ * table has both a `(locale_group_id, locale)` pair and a per-locale one-to-one
20
+ * relation index, and reporting either as the other tells the user to fix
21
+ * something that is not wrong. Postgres names the offender on `constraint`;
22
+ * the same defensive `cause` walk as {@link isUniqueViolation} finds it through
23
+ * whatever the driver/ORM wrapped it in.
24
+ *
25
+ * Returns `''` for a violation whose constraint the driver did not name, so a
26
+ * caller can still distinguish "a unique violation, unattributed" from "not a
27
+ * unique violation" without a second call.
28
+ */
29
+ export declare function violatedConstraint(error: unknown): string | undefined;
30
+ /**
31
+ * Whether an error (or its `cause`, where a driver/ORM wraps it) is a Postgres
32
+ * **foreign-key** violation (`23503`).
33
+ *
34
+ * The counterpart of {@link isUniqueViolation} for the other constraint a write
35
+ * can lose to. A row that something else still references under
36
+ * `ON DELETE RESTRICT` is a legitimate refusal the caller can act on ("detach
37
+ * the referring records first") — leaking it as a 500 tells them the server
38
+ * broke instead. Deliberately a boolean and not a constraint name: unlike the
39
+ * several unique indexes on one localized content table, the answer here does
40
+ * not vary by which FK tripped.
41
+ */
42
+ export declare function isForeignKeyViolation(error: unknown): boolean;
43
+ //# sourceMappingURL=pg-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-errors.d.ts","sourceRoot":"","sources":["../../src/lib/pg-errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAiBrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB7D"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ /**
3
+ * Postgres error introspection shared by services that map constraint
4
+ * violations to clean HTTP errors (e.g. a unique-index race settled by the
5
+ * constraint → 409) instead of leaking a 500.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.isUniqueViolation = isUniqueViolation;
9
+ exports.violatedConstraint = violatedConstraint;
10
+ exports.isForeignKeyViolation = isForeignKeyViolation;
11
+ /** Postgres error code for a unique constraint/index violation. */
12
+ const UNIQUE_VIOLATION = '23505';
13
+ /** Postgres error code for a foreign-key constraint violation. */
14
+ const FOREIGN_KEY_VIOLATION = '23503';
15
+ /**
16
+ * Whether an error (or its `cause`, where a driver/ORM wraps it) is a
17
+ * Postgres unique violation (`23505`). Drivers surface the SQLSTATE on a
18
+ * `code` property; the walk is defensive because Drizzle has wrapped driver
19
+ * errors differently across versions.
20
+ */
21
+ function isUniqueViolation(error) {
22
+ return violatedConstraint(error) !== undefined;
23
+ }
24
+ /**
25
+ * The **name of the index/constraint** a unique violation (`23505`) tripped, or
26
+ * `undefined` when the error is not one.
27
+ *
28
+ * A table can carry several unique indexes, and "which one" is the difference
29
+ * between two entirely different messages to the caller — a localized content
30
+ * table has both a `(locale_group_id, locale)` pair and a per-locale one-to-one
31
+ * relation index, and reporting either as the other tells the user to fix
32
+ * something that is not wrong. Postgres names the offender on `constraint`;
33
+ * the same defensive `cause` walk as {@link isUniqueViolation} finds it through
34
+ * whatever the driver/ORM wrapped it in.
35
+ *
36
+ * Returns `''` for a violation whose constraint the driver did not name, so a
37
+ * caller can still distinguish "a unique violation, unattributed" from "not a
38
+ * unique violation" without a second call.
39
+ */
40
+ function violatedConstraint(error) {
41
+ let current = error;
42
+ for (let depth = 0; current && depth < 5; depth += 1) {
43
+ if (typeof current === 'object' &&
44
+ 'code' in current &&
45
+ current.code === UNIQUE_VIOLATION) {
46
+ const name = current.constraint;
47
+ return typeof name === 'string' ? name : '';
48
+ }
49
+ current =
50
+ typeof current === 'object' && 'cause' in current
51
+ ? current.cause
52
+ : undefined;
53
+ }
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Whether an error (or its `cause`, where a driver/ORM wraps it) is a Postgres
58
+ * **foreign-key** violation (`23503`).
59
+ *
60
+ * The counterpart of {@link isUniqueViolation} for the other constraint a write
61
+ * can lose to. A row that something else still references under
62
+ * `ON DELETE RESTRICT` is a legitimate refusal the caller can act on ("detach
63
+ * the referring records first") — leaking it as a 500 tells them the server
64
+ * broke instead. Deliberately a boolean and not a constraint name: unlike the
65
+ * several unique indexes on one localized content table, the answer here does
66
+ * not vary by which FK tripped.
67
+ */
68
+ function isForeignKeyViolation(error) {
69
+ let current = error;
70
+ for (let depth = 0; current && depth < 5; depth += 1) {
71
+ if (typeof current === 'object' &&
72
+ 'code' in current &&
73
+ current.code === FOREIGN_KEY_VIOLATION) {
74
+ return true;
75
+ }
76
+ current =
77
+ typeof current === 'object' && 'cause' in current
78
+ ? current.cause
79
+ : undefined;
80
+ }
81
+ return false;
82
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@orthacms/utils-server",
3
+ "version": "0.0.0-reserve.0",
4
+ "description": "@orthacms/utils-server — part of Ortha CMS.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/utils/server",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ortha-source/ortha-cms.git",
10
+ "directory": "packages/utils/server"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ortha-source/ortha-cms/issues"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "dependencies": {
28
+ "@nestjs/common": "^11.0.0",
29
+ "drizzle-orm": "^0.45.0",
30
+ "tslib": "^2.3.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }