@classytic/repo-core 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 (84) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/dist/cache/index.d.mts +4 -0
  5. package/dist/cache/index.mjs +3 -0
  6. package/dist/cache/memory-adapter.d.mts +7 -0
  7. package/dist/cache/memory-adapter.mjs +37 -0
  8. package/dist/cache/stable-stringify.d.mts +15 -0
  9. package/dist/cache/stable-stringify.mjs +19 -0
  10. package/dist/cache/types.d.mts +59 -0
  11. package/dist/context/index.d.mts +2 -0
  12. package/dist/context/index.mjs +0 -0
  13. package/dist/context/types.d.mts +24 -0
  14. package/dist/errors/create-error.d.mts +19 -0
  15. package/dist/errors/create-error.mjs +23 -0
  16. package/dist/errors/duplicate-key.d.mts +38 -0
  17. package/dist/errors/duplicate-key.mjs +57 -0
  18. package/dist/errors/index.d.mts +4 -0
  19. package/dist/errors/index.mjs +3 -0
  20. package/dist/errors/types.d.mts +37 -0
  21. package/dist/filter/builders.d.mts +60 -0
  22. package/dist/filter/builders.mjs +172 -0
  23. package/dist/filter/guard.d.mts +13 -0
  24. package/dist/filter/guard.mjs +34 -0
  25. package/dist/filter/index.d.mts +7 -0
  26. package/dist/filter/index.mjs +6 -0
  27. package/dist/filter/match.d.mts +12 -0
  28. package/dist/filter/match.mjs +91 -0
  29. package/dist/filter/scope.d.mts +31 -0
  30. package/dist/filter/scope.mjs +54 -0
  31. package/dist/filter/types.d.mts +143 -0
  32. package/dist/filter/walk.d.mts +24 -0
  33. package/dist/filter/walk.mjs +77 -0
  34. package/dist/hooks/engine.d.mts +48 -0
  35. package/dist/hooks/engine.mjs +101 -0
  36. package/dist/hooks/events.d.mts +95 -0
  37. package/dist/hooks/events.mjs +93 -0
  38. package/dist/hooks/index.d.mts +5 -0
  39. package/dist/hooks/index.mjs +4 -0
  40. package/dist/hooks/priority.d.mts +23 -0
  41. package/dist/hooks/priority.mjs +21 -0
  42. package/dist/hooks/types.d.mts +37 -0
  43. package/dist/lookup/index.d.mts +2 -0
  44. package/dist/lookup/index.mjs +0 -0
  45. package/dist/lookup/types.d.mts +170 -0
  46. package/dist/operations/index.d.mts +3 -0
  47. package/dist/operations/index.mjs +2 -0
  48. package/dist/operations/registry.d.mts +41 -0
  49. package/dist/operations/registry.mjs +140 -0
  50. package/dist/operations/types.d.mts +49 -0
  51. package/dist/pagination/cursor.d.mts +44 -0
  52. package/dist/pagination/cursor.mjs +150 -0
  53. package/dist/pagination/index.d.mts +5 -0
  54. package/dist/pagination/index.mjs +4 -0
  55. package/dist/pagination/keyset.d.mts +25 -0
  56. package/dist/pagination/keyset.mjs +61 -0
  57. package/dist/pagination/offset.d.mts +26 -0
  58. package/dist/pagination/offset.mjs +47 -0
  59. package/dist/pagination/types.d.mts +136 -0
  60. package/dist/query-parser/coerce.d.mts +16 -0
  61. package/dist/query-parser/coerce.mjs +73 -0
  62. package/dist/query-parser/index.d.mts +4 -0
  63. package/dist/query-parser/index.mjs +3 -0
  64. package/dist/query-parser/parse-url.d.mts +7 -0
  65. package/dist/query-parser/parse-url.mjs +224 -0
  66. package/dist/query-parser/types.d.mts +104 -0
  67. package/dist/repository/base.d.mts +90 -0
  68. package/dist/repository/base.mjs +111 -0
  69. package/dist/repository/index.d.mts +5 -0
  70. package/dist/repository/index.mjs +3 -0
  71. package/dist/repository/plugin-types.d.mts +27 -0
  72. package/dist/repository/plugin-types.mjs +45 -0
  73. package/dist/repository/types.d.mts +470 -0
  74. package/dist/schema/field-rules.d.mts +62 -0
  75. package/dist/schema/field-rules.mjs +110 -0
  76. package/dist/schema/index.d.mts +3 -0
  77. package/dist/schema/index.mjs +2 -0
  78. package/dist/schema/types.d.mts +138 -0
  79. package/dist/testing/conformance.d.mts +6 -0
  80. package/dist/testing/conformance.mjs +481 -0
  81. package/dist/testing/index.d.mts +3 -0
  82. package/dist/testing/index.mjs +2 -0
  83. package/dist/testing/types.d.mts +113 -0
  84. package/package.json +130 -0
@@ -0,0 +1,60 @@
1
+ import { Filter, FilterEq, FilterExists, FilterFalse, FilterGt, FilterGte, FilterIn, FilterLike, FilterLt, FilterLte, FilterNe, FilterNin, FilterRaw, FilterRegex, FilterTrue } from "./types.mjs";
2
+
3
+ //#region src/filter/builders.d.ts
4
+ declare const eq: (field: string, value: unknown) => FilterEq;
5
+ declare const ne: (field: string, value: unknown) => FilterNe;
6
+ declare const gt: (field: string, value: unknown) => FilterGt;
7
+ declare const gte: (field: string, value: unknown) => FilterGte;
8
+ declare const lt: (field: string, value: unknown) => FilterLt;
9
+ declare const lte: (field: string, value: unknown) => FilterLte;
10
+ /** Membership (`$in` / `IN (...)`). Aliased as `anyOf` for readability. */
11
+ declare const in_: (field: string, values: readonly unknown[]) => FilterIn;
12
+ /** Non-membership (`$nin` / `NOT IN (...)`). Aliased as `noneOf`. */
13
+ declare const nin: (field: string, values: readonly unknown[]) => FilterNin;
14
+ declare const like: (field: string, pattern: string, caseSensitivity?: "sensitive" | "insensitive") => FilterLike;
15
+ declare const regex: (field: string, pattern: string, flags?: string) => FilterRegex;
16
+ declare const exists: (field: string, present?: boolean) => FilterExists;
17
+ /**
18
+ * Conjunction. Normalizes trivially: an empty `and` returns `TRUE`, a
19
+ * single-child `and` is flattened to the child. Plugins that progressively
20
+ * build a filter (`let f = TRUE; if (...) f = and(f, eq(...))`) land in
21
+ * the expected shape without manual tree surgery.
22
+ */
23
+ declare function and(...children: Filter[]): Filter;
24
+ /** Disjunction. Boolean algebra duals of `and`: TRUE absorbs, FALSE is identity. */
25
+ declare function or(...children: Filter[]): Filter;
26
+ /** Negation. Double-negation is eliminated (`not(not(x)) === x`). */
27
+ declare function not(child: Filter): Filter;
28
+ /** Matches every document. Identity element for `and`. */
29
+ declare const TRUE: FilterTrue;
30
+ /** Matches no document. Identity element for `or`. */
31
+ declare const FALSE: FilterFalse;
32
+ /** Inclusive range: `lo <= field <= hi`. Desugars to `and(gte, lte)`. */
33
+ declare function between(field: string, lo: unknown, hi: unknown): Filter;
34
+ /** Case-insensitive by default. Escapes `%` and `_` in `prefix` so they match literally. */
35
+ declare function startsWith(field: string, prefix: string, caseSensitivity?: 'sensitive' | 'insensitive'): FilterLike;
36
+ declare function endsWith(field: string, suffix: string, caseSensitivity?: 'sensitive' | 'insensitive'): FilterLike;
37
+ /** Substring match. Case-insensitive by default. */
38
+ declare function contains(field: string, substring: string, caseSensitivity?: 'sensitive' | 'insensitive'): FilterLike;
39
+ /**
40
+ * Case-insensitive equality. Desugars to a case-insensitive `like` with
41
+ * the value's SQL wildcards escaped — kits compile it the same as an
42
+ * equality check against `lower(field) = lower(?)`.
43
+ */
44
+ declare function iEq(field: string, value: string): FilterLike;
45
+ /** Shorthand for `exists(field, false)`. Reads better in plugin code. */
46
+ declare function isNull(field: string): FilterExists;
47
+ /** Shorthand for `exists(field, true)`. */
48
+ declare function isNotNull(field: string): FilterExists;
49
+ /**
50
+ * Embed a driver-native fragment verbatim. Use for features the IR
51
+ * doesn't express: pgvector similarity, SQLite JSON1 path access,
52
+ * Mongo `$geoWithin`, etc.
53
+ *
54
+ * `matchFilter` (the in-memory evaluator) returns `false` for raw nodes
55
+ * because evaluating arbitrary SQL in JS isn't possible — make sure
56
+ * predicates used in client-side filtering don't rely on `raw`.
57
+ */
58
+ declare function raw(sql: string, params?: readonly unknown[]): FilterRaw;
59
+ //#endregion
60
+ export { FALSE, TRUE, and, between, contains, endsWith, eq, exists, gt, gte, iEq, in_, isNotNull, isNull, like, lt, lte, ne, nin, not, or, raw, regex, startsWith };
@@ -0,0 +1,172 @@
1
+ //#region src/filter/builders.ts
2
+ const eq = (field, value) => Object.freeze({
3
+ op: "eq",
4
+ field,
5
+ value
6
+ });
7
+ const ne = (field, value) => Object.freeze({
8
+ op: "ne",
9
+ field,
10
+ value
11
+ });
12
+ const gt = (field, value) => Object.freeze({
13
+ op: "gt",
14
+ field,
15
+ value
16
+ });
17
+ const gte = (field, value) => Object.freeze({
18
+ op: "gte",
19
+ field,
20
+ value
21
+ });
22
+ const lt = (field, value) => Object.freeze({
23
+ op: "lt",
24
+ field,
25
+ value
26
+ });
27
+ const lte = (field, value) => Object.freeze({
28
+ op: "lte",
29
+ field,
30
+ value
31
+ });
32
+ /** Membership (`$in` / `IN (...)`). Aliased as `anyOf` for readability. */
33
+ const in_ = (field, values) => Object.freeze({
34
+ op: "in",
35
+ field,
36
+ values: Object.freeze([...values])
37
+ });
38
+ /** Non-membership (`$nin` / `NOT IN (...)`). Aliased as `noneOf`. */
39
+ const nin = (field, values) => Object.freeze({
40
+ op: "nin",
41
+ field,
42
+ values: Object.freeze([...values])
43
+ });
44
+ const like = (field, pattern, caseSensitivity = "insensitive") => Object.freeze({
45
+ op: "like",
46
+ field,
47
+ pattern,
48
+ caseSensitivity
49
+ });
50
+ const regex = (field, pattern, flags) => {
51
+ const node = {
52
+ op: "regex",
53
+ field,
54
+ pattern,
55
+ ...flags !== void 0 && { flags }
56
+ };
57
+ return Object.freeze(node);
58
+ };
59
+ const exists = (field, present = true) => Object.freeze({
60
+ op: "exists",
61
+ field,
62
+ exists: present
63
+ });
64
+ /**
65
+ * Conjunction. Normalizes trivially: an empty `and` returns `TRUE`, a
66
+ * single-child `and` is flattened to the child. Plugins that progressively
67
+ * build a filter (`let f = TRUE; if (...) f = and(f, eq(...))`) land in
68
+ * the expected shape without manual tree surgery.
69
+ */
70
+ function and(...children) {
71
+ const flat = [];
72
+ for (const c of children) if (c.op === "and") flat.push(...c.children);
73
+ else flat.push(c);
74
+ const filtered = [];
75
+ for (const c of flat) {
76
+ if (c.op === "false") return FALSE;
77
+ if (c.op === "true") continue;
78
+ filtered.push(c);
79
+ }
80
+ if (filtered.length === 0) return TRUE;
81
+ if (filtered.length === 1) return filtered[0];
82
+ const node = {
83
+ op: "and",
84
+ children: Object.freeze(filtered)
85
+ };
86
+ return Object.freeze(node);
87
+ }
88
+ /** Disjunction. Boolean algebra duals of `and`: TRUE absorbs, FALSE is identity. */
89
+ function or(...children) {
90
+ const flat = [];
91
+ for (const c of children) if (c.op === "or") flat.push(...c.children);
92
+ else flat.push(c);
93
+ const filtered = [];
94
+ for (const c of flat) {
95
+ if (c.op === "true") return TRUE;
96
+ if (c.op === "false") continue;
97
+ filtered.push(c);
98
+ }
99
+ if (filtered.length === 0) return FALSE;
100
+ if (filtered.length === 1) return filtered[0];
101
+ const node = {
102
+ op: "or",
103
+ children: Object.freeze(filtered)
104
+ };
105
+ return Object.freeze(node);
106
+ }
107
+ /** Negation. Double-negation is eliminated (`not(not(x)) === x`). */
108
+ function not(child) {
109
+ if (child.op === "not") return child.child;
110
+ if (child.op === "true") return FALSE;
111
+ if (child.op === "false") return TRUE;
112
+ return Object.freeze({
113
+ op: "not",
114
+ child
115
+ });
116
+ }
117
+ /** Matches every document. Identity element for `and`. */
118
+ const TRUE = Object.freeze({ op: "true" });
119
+ /** Matches no document. Identity element for `or`. */
120
+ const FALSE = Object.freeze({ op: "false" });
121
+ /** Inclusive range: `lo <= field <= hi`. Desugars to `and(gte, lte)`. */
122
+ function between(field, lo, hi) {
123
+ return and(gte(field, lo), lte(field, hi));
124
+ }
125
+ /** Case-insensitive by default. Escapes `%` and `_` in `prefix` so they match literally. */
126
+ function startsWith(field, prefix, caseSensitivity = "insensitive") {
127
+ return like(field, `${escapeLikePattern(prefix)}%`, caseSensitivity);
128
+ }
129
+ function endsWith(field, suffix, caseSensitivity = "insensitive") {
130
+ return like(field, `%${escapeLikePattern(suffix)}`, caseSensitivity);
131
+ }
132
+ /** Substring match. Case-insensitive by default. */
133
+ function contains(field, substring, caseSensitivity = "insensitive") {
134
+ return like(field, `%${escapeLikePattern(substring)}%`, caseSensitivity);
135
+ }
136
+ /**
137
+ * Case-insensitive equality. Desugars to a case-insensitive `like` with
138
+ * the value's SQL wildcards escaped — kits compile it the same as an
139
+ * equality check against `lower(field) = lower(?)`.
140
+ */
141
+ function iEq(field, value) {
142
+ return like(field, escapeLikePattern(value), "insensitive");
143
+ }
144
+ /** Shorthand for `exists(field, false)`. Reads better in plugin code. */
145
+ function isNull(field) {
146
+ return exists(field, false);
147
+ }
148
+ /** Shorthand for `exists(field, true)`. */
149
+ function isNotNull(field) {
150
+ return exists(field, true);
151
+ }
152
+ /**
153
+ * Embed a driver-native fragment verbatim. Use for features the IR
154
+ * doesn't express: pgvector similarity, SQLite JSON1 path access,
155
+ * Mongo `$geoWithin`, etc.
156
+ *
157
+ * `matchFilter` (the in-memory evaluator) returns `false` for raw nodes
158
+ * because evaluating arbitrary SQL in JS isn't possible — make sure
159
+ * predicates used in client-side filtering don't rely on `raw`.
160
+ */
161
+ function raw(sql, params = []) {
162
+ return Object.freeze({
163
+ op: "raw",
164
+ sql,
165
+ params: Object.freeze([...params])
166
+ });
167
+ }
168
+ function escapeLikePattern(value) {
169
+ return value.replace(/[%_]/g, (c) => `\\${c}`);
170
+ }
171
+ //#endregion
172
+ export { FALSE, TRUE, and, between, contains, endsWith, eq, exists, gt, gte, iEq, in_, isNotNull, isNull, like, lt, lte, ne, nin, not, or, raw, regex, startsWith };
@@ -0,0 +1,13 @@
1
+ import { Filter } from "./types.mjs";
2
+
3
+ //#region src/filter/guard.d.ts
4
+ /**
5
+ * True when `value` is a Filter IR node.
6
+ *
7
+ * Structural check: requires `op` to be a known filter operator string.
8
+ * Deeper validation (children are themselves filters, field is a string,
9
+ * values is an array) is left to the compiler — this is the fast-path gate.
10
+ */
11
+ declare function isFilter(value: unknown): value is Filter;
12
+ //#endregion
13
+ export { isFilter };
@@ -0,0 +1,34 @@
1
+ //#region src/filter/guard.ts
2
+ const FILTER_OPS = new Set([
3
+ "eq",
4
+ "ne",
5
+ "gt",
6
+ "gte",
7
+ "lt",
8
+ "lte",
9
+ "in",
10
+ "nin",
11
+ "like",
12
+ "regex",
13
+ "exists",
14
+ "and",
15
+ "or",
16
+ "not",
17
+ "true",
18
+ "false",
19
+ "raw"
20
+ ]);
21
+ /**
22
+ * True when `value` is a Filter IR node.
23
+ *
24
+ * Structural check: requires `op` to be a known filter operator string.
25
+ * Deeper validation (children are themselves filters, field is a string,
26
+ * values is an array) is left to the compiler — this is the fast-path gate.
27
+ */
28
+ function isFilter(value) {
29
+ if (!value || typeof value !== "object") return false;
30
+ const op = value.op;
31
+ return typeof op === "string" && FILTER_OPS.has(op);
32
+ }
33
+ //#endregion
34
+ export { isFilter };
@@ -0,0 +1,7 @@
1
+ import { Filter, FilterAnd, FilterEq, FilterExists, FilterFalse, FilterGt, FilterGte, FilterIn, FilterLike, FilterLt, FilterLte, FilterNe, FilterNin, FilterNot, FilterOp, FilterOr, FilterRaw, FilterRegex, FilterTrue } from "./types.mjs";
2
+ import { FALSE, TRUE, and, between, contains, endsWith, eq, exists, gt, gte, iEq, in_, isNotNull, isNull, like, lt, lte, ne, nin, not, or, raw, regex, startsWith } from "./builders.mjs";
3
+ import { isFilter } from "./guard.mjs";
4
+ import { asPredicate, matchFilter } from "./match.mjs";
5
+ import { SCOPE_ANY, buildTenantScope, mergeScope } from "./scope.mjs";
6
+ import { collectFields, mapFilter, walkFilter } from "./walk.mjs";
7
+ export { FALSE, type Filter, type FilterAnd, type FilterEq, type FilterExists, type FilterFalse, type FilterGt, type FilterGte, type FilterIn, type FilterLike, type FilterLt, type FilterLte, type FilterNe, type FilterNin, type FilterNot, type FilterOp, type FilterOr, type FilterRaw, type FilterRegex, type FilterTrue, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, mergeScope, ne, nin, nin as noneOf, not, or, raw, regex, startsWith, walkFilter };
@@ -0,0 +1,6 @@
1
+ import { FALSE, TRUE, and, between, contains, endsWith, eq, exists, gt, gte, iEq, in_, isNotNull, isNull, like, lt, lte, ne, nin, not, or, raw, regex, startsWith } from "./builders.mjs";
2
+ import { isFilter } from "./guard.mjs";
3
+ import { asPredicate, matchFilter } from "./match.mjs";
4
+ import { SCOPE_ANY, buildTenantScope, mergeScope } from "./scope.mjs";
5
+ import { collectFields, mapFilter, walkFilter } from "./walk.mjs";
6
+ export { FALSE, SCOPE_ANY, TRUE, and, in_ as anyOf, asPredicate, between, buildTenantScope, collectFields, contains, endsWith, eq, exists, gt, gte, iEq, in_, not as invert, isFilter, isNotNull, isNull, like, lt, lte, mapFilter, matchFilter, mergeScope, ne, nin, nin as noneOf, not, or, raw, regex, startsWith, walkFilter };
@@ -0,0 +1,12 @@
1
+ import { Filter } from "./types.mjs";
2
+
3
+ //#region src/filter/match.d.ts
4
+ /** Evaluate a filter against a single document. Returns true iff the doc matches. */
5
+ declare function matchFilter(doc: unknown, filter: Filter): boolean;
6
+ /**
7
+ * Lift a Filter into a standalone predicate closure. Handy for
8
+ * `array.filter(asPredicate(myFilter))`.
9
+ */
10
+ declare function asPredicate<T>(filter: Filter): (doc: T) => boolean;
11
+ //#endregion
12
+ export { asPredicate, matchFilter };
@@ -0,0 +1,91 @@
1
+ //#region src/filter/match.ts
2
+ /** Evaluate a filter against a single document. Returns true iff the doc matches. */
3
+ function matchFilter(doc, filter) {
4
+ switch (filter.op) {
5
+ case "true": return true;
6
+ case "false": return false;
7
+ case "and": return filter.children.every((child) => matchFilter(doc, child));
8
+ case "or": return filter.children.some((child) => matchFilter(doc, child));
9
+ case "not": return !matchFilter(doc, filter.child);
10
+ case "eq": return equals(getField(doc, filter.field), filter.value);
11
+ case "ne": return !equals(getField(doc, filter.field), filter.value);
12
+ case "gt": return compare(getField(doc, filter.field), filter.value) > 0;
13
+ case "gte": return compare(getField(doc, filter.field), filter.value) >= 0;
14
+ case "lt": return compare(getField(doc, filter.field), filter.value) < 0;
15
+ case "lte": return compare(getField(doc, filter.field), filter.value) <= 0;
16
+ case "in": {
17
+ const v = getField(doc, filter.field);
18
+ return filter.values.some((candidate) => equals(v, candidate));
19
+ }
20
+ case "nin": {
21
+ const v = getField(doc, filter.field);
22
+ return !filter.values.some((candidate) => equals(v, candidate));
23
+ }
24
+ case "exists": {
25
+ const v = getField(doc, filter.field);
26
+ const present = v !== void 0 && v !== null;
27
+ return filter.exists ? present : !present;
28
+ }
29
+ case "like": {
30
+ const v = getField(doc, filter.field);
31
+ if (typeof v !== "string") return false;
32
+ const flags = filter.caseSensitivity === "sensitive" ? "" : "i";
33
+ return new RegExp(`^${likeToRegex(filter.pattern)}$`, flags).test(v);
34
+ }
35
+ case "regex": {
36
+ const v = getField(doc, filter.field);
37
+ if (typeof v !== "string") return false;
38
+ return new RegExp(filter.pattern, filter.flags).test(v);
39
+ }
40
+ case "raw": return false;
41
+ }
42
+ }
43
+ /**
44
+ * Lift a Filter into a standalone predicate closure. Handy for
45
+ * `array.filter(asPredicate(myFilter))`.
46
+ */
47
+ function asPredicate(filter) {
48
+ return (doc) => matchFilter(doc, filter);
49
+ }
50
+ function getField(doc, path) {
51
+ if (!doc || typeof doc !== "object") return void 0;
52
+ const segments = path.split(".");
53
+ let cursor = doc;
54
+ for (const segment of segments) {
55
+ if (cursor === null || cursor === void 0) return void 0;
56
+ if (typeof cursor !== "object") return void 0;
57
+ cursor = cursor[segment];
58
+ }
59
+ return cursor;
60
+ }
61
+ function equals(a, b) {
62
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
63
+ if (a instanceof Date && typeof b === "string") return a.toISOString() === b;
64
+ if (b instanceof Date && typeof a === "string") return b.toISOString() === a;
65
+ return a === b;
66
+ }
67
+ function compare(a, b) {
68
+ const aNum = toComparable(a);
69
+ const bNum = toComparable(b);
70
+ if (aNum === void 0 || bNum === void 0) return NaN;
71
+ if (aNum < bNum) return -1;
72
+ if (aNum > bNum) return 1;
73
+ return 0;
74
+ }
75
+ function toComparable(value) {
76
+ if (value === null || value === void 0) return void 0;
77
+ if (value instanceof Date) return value.getTime();
78
+ if (typeof value === "number" || typeof value === "string") return value;
79
+ if (typeof value === "boolean") return value ? 1 : 0;
80
+ }
81
+ /** SQL `LIKE` pattern → JS regex body. Escapes regex metachars; `%` → `.*`, `_` → `.`. */
82
+ function likeToRegex(pattern) {
83
+ let out = "";
84
+ for (const ch of pattern) if (ch === "%") out += ".*";
85
+ else if (ch === "_") out += ".";
86
+ else if (/[.*+?^${}()|[\]\\]/.test(ch)) out += `\\${ch}`;
87
+ else out += ch;
88
+ return out;
89
+ }
90
+ //#endregion
91
+ export { asPredicate, matchFilter };
@@ -0,0 +1,31 @@
1
+ import { Filter } from "./types.mjs";
2
+
3
+ //#region src/filter/scope.d.ts
4
+ /**
5
+ * Inject a tenant-scope predicate into an existing filter.
6
+ *
7
+ * Returns a new filter representing `existing AND tenantField = tenantId`.
8
+ * Handles three existing-filter shapes:
9
+ * - `undefined` → returns the bare scope predicate
10
+ * - Filter IR → ANDed onto the tree
11
+ * - flat record `{ field: value }` → merged field-wise (each existing
12
+ * key becomes an `eq` node, ANDed with the scope)
13
+ *
14
+ * Used by each kit's `multiTenantPlugin.apply()` to stay consistent
15
+ * without duplicating the merge logic.
16
+ */
17
+ declare function buildTenantScope(existing: Filter | Record<string, unknown> | undefined, tenantField: string, tenantId: string | number): Filter | Record<string, unknown>;
18
+ /**
19
+ * Merge an arbitrary scope predicate into an existing filter.
20
+ *
21
+ * Generalization of `buildTenantScope` — callers pass any Filter IR node
22
+ * (e.g. `isNull('deletedAt')` for soft-delete, `eq('status', 'active')`
23
+ * for policy guards) and this function handles the existing-filter shape
24
+ * matrix. Flat-record existing filters become Filter IR on the way out so
25
+ * the result is always `Filter`-compatible.
26
+ */
27
+ declare function mergeScope(existing: Filter | Record<string, unknown> | undefined, scope: Filter): Filter;
28
+ /** Convenience: scope that matches everything — the identity under AND. */
29
+ declare const SCOPE_ANY: Filter;
30
+ //#endregion
31
+ export { SCOPE_ANY, buildTenantScope, mergeScope };
@@ -0,0 +1,54 @@
1
+ import { TRUE, and, eq } from "./builders.mjs";
2
+ import { isFilter } from "./guard.mjs";
3
+ //#region src/filter/scope.ts
4
+ /**
5
+ * Scope-injection helpers.
6
+ *
7
+ * Every policy plugin (multi-tenant, soft-delete, org-boundary) follows the
8
+ * same pattern: AND a predicate into an existing filter while tolerating
9
+ * Filter IR, flat `{ field: value }` records, and `undefined`. These
10
+ * helpers lift that pattern out so every kit's local policy plugins compose
11
+ * the same way.
12
+ */
13
+ /**
14
+ * Inject a tenant-scope predicate into an existing filter.
15
+ *
16
+ * Returns a new filter representing `existing AND tenantField = tenantId`.
17
+ * Handles three existing-filter shapes:
18
+ * - `undefined` → returns the bare scope predicate
19
+ * - Filter IR → ANDed onto the tree
20
+ * - flat record `{ field: value }` → merged field-wise (each existing
21
+ * key becomes an `eq` node, ANDed with the scope)
22
+ *
23
+ * Used by each kit's `multiTenantPlugin.apply()` to stay consistent
24
+ * without duplicating the merge logic.
25
+ */
26
+ function buildTenantScope(existing, tenantField, tenantId) {
27
+ const scope = eq(tenantField, tenantId);
28
+ if (existing === void 0) return scope;
29
+ if (isFilter(existing)) return existing.op === "true" ? scope : and(existing, scope);
30
+ return {
31
+ ...existing,
32
+ [tenantField]: tenantId
33
+ };
34
+ }
35
+ /**
36
+ * Merge an arbitrary scope predicate into an existing filter.
37
+ *
38
+ * Generalization of `buildTenantScope` — callers pass any Filter IR node
39
+ * (e.g. `isNull('deletedAt')` for soft-delete, `eq('status', 'active')`
40
+ * for policy guards) and this function handles the existing-filter shape
41
+ * matrix. Flat-record existing filters become Filter IR on the way out so
42
+ * the result is always `Filter`-compatible.
43
+ */
44
+ function mergeScope(existing, scope) {
45
+ if (existing === void 0) return scope;
46
+ if (isFilter(existing)) return existing.op === "true" ? scope : and(existing, scope);
47
+ const eqs = Object.entries(existing).map(([f, v]) => eq(f, v));
48
+ if (eqs.length === 0) return scope;
49
+ return and(...eqs, scope);
50
+ }
51
+ /** Convenience: scope that matches everything — the identity under AND. */
52
+ const SCOPE_ANY = TRUE;
53
+ //#endregion
54
+ export { SCOPE_ANY, buildTenantScope, mergeScope };
@@ -0,0 +1,143 @@
1
+ //#region src/filter/types.d.ts
2
+ /**
3
+ * Filter IR — driver-agnostic query AST.
4
+ *
5
+ * A `Filter` is a tagged-union tree that every kit knows how to compile
6
+ * to its native syntax. Plugins compose `Filter` nodes via the combinators
7
+ * in `builders.ts` rather than writing `$`-operator objects directly, so
8
+ * policy hooks (multi-tenant scope, soft-delete) work identically against
9
+ * MongoDB, SQLite, Postgres, and Prisma.
10
+ *
11
+ * The IR is intentionally small — it covers the operator surface every
12
+ * SQL and document store supports. Backend-specific operators (Mongo
13
+ * `$geoWithin`, pgvector `<=>`, Postgres JSONB `@>`) stay kit-native and
14
+ * are composed alongside the IR via each kit's escape hatch, not embedded
15
+ * in this type.
16
+ *
17
+ * **Compat invariant:** mongokit's Mongo-shaped filter objects (`$`-keyed
18
+ * records) are NOT `Filter` values. Kits accept both via a passthrough in
19
+ * their compiler — a raw record is treated as pre-compiled and handed to
20
+ * the driver unchanged. See `isFilter` for the runtime guard.
21
+ */
22
+ /** Leaf operator on a single field with a scalar-ish value. */
23
+ interface FilterEq {
24
+ readonly op: 'eq';
25
+ readonly field: string;
26
+ readonly value: unknown;
27
+ }
28
+ interface FilterNe {
29
+ readonly op: 'ne';
30
+ readonly field: string;
31
+ readonly value: unknown;
32
+ }
33
+ interface FilterGt {
34
+ readonly op: 'gt';
35
+ readonly field: string;
36
+ readonly value: unknown;
37
+ }
38
+ interface FilterGte {
39
+ readonly op: 'gte';
40
+ readonly field: string;
41
+ readonly value: unknown;
42
+ }
43
+ interface FilterLt {
44
+ readonly op: 'lt';
45
+ readonly field: string;
46
+ readonly value: unknown;
47
+ }
48
+ interface FilterLte {
49
+ readonly op: 'lte';
50
+ readonly field: string;
51
+ readonly value: unknown;
52
+ }
53
+ /** Leaf operator on a single field with a set value. */
54
+ interface FilterIn {
55
+ readonly op: 'in';
56
+ readonly field: string;
57
+ readonly values: readonly unknown[];
58
+ }
59
+ interface FilterNin {
60
+ readonly op: 'nin';
61
+ readonly field: string;
62
+ readonly values: readonly unknown[];
63
+ }
64
+ /**
65
+ * Substring / prefix / suffix match with SQL-style `%` wildcards.
66
+ * Kits compile to MongoDB `$regex`, SQL `LIKE`, Prisma `contains`/`startsWith`/`endsWith`.
67
+ * Use `regex` for anchored/character-class patterns when your schema allows.
68
+ */
69
+ interface FilterLike {
70
+ readonly op: 'like';
71
+ readonly field: string;
72
+ readonly pattern: string;
73
+ /** Case sensitivity. Default `'insensitive'` — matches how most UIs expect search. */
74
+ readonly caseSensitivity?: 'sensitive' | 'insensitive';
75
+ }
76
+ /**
77
+ * Regex match — the most powerful leaf. Not every driver exposes the same
78
+ * regex dialect (Mongo ICU, Postgres POSIX, SQLite PCRE via extension),
79
+ * so kits MAY reject patterns their backend can't compile. Prefer `like`
80
+ * when a substring match suffices.
81
+ */
82
+ interface FilterRegex {
83
+ readonly op: 'regex';
84
+ readonly field: string;
85
+ readonly pattern: string;
86
+ readonly flags?: string;
87
+ }
88
+ /** Field presence / absence (NULL vs NOT NULL in SQL; `$exists` in Mongo). */
89
+ interface FilterExists {
90
+ readonly op: 'exists';
91
+ readonly field: string;
92
+ readonly exists: boolean;
93
+ }
94
+ /** Boolean composition nodes — recursive. */
95
+ interface FilterAnd {
96
+ readonly op: 'and';
97
+ readonly children: readonly Filter[];
98
+ }
99
+ interface FilterOr {
100
+ readonly op: 'or';
101
+ readonly children: readonly Filter[];
102
+ }
103
+ interface FilterNot {
104
+ readonly op: 'not';
105
+ readonly child: Filter;
106
+ }
107
+ /** Tautology — matches every document. Useful as an identity for `and` reductions. */
108
+ interface FilterTrue {
109
+ readonly op: 'true';
110
+ }
111
+ /** Contradiction — matches no document. */
112
+ interface FilterFalse {
113
+ readonly op: 'false';
114
+ }
115
+ /**
116
+ * Driver-native escape hatch. Carries opaque SQL / query fragment + params
117
+ * through to the compiler untouched. Kits compile by inlining the fragment
118
+ * verbatim (and appending the params in order). Use sparingly — the IR
119
+ * advantage is lost inside a `raw` node.
120
+ *
121
+ * Arc's `matchFilter` (in-memory) always returns `false` for `raw` nodes:
122
+ * evaluating arbitrary SQL in JS isn't possible, so a caller who uses raw
123
+ * must either (a) not rely on in-memory matching for that predicate, or
124
+ * (b) provide their own evaluator via the match options surface.
125
+ *
126
+ * @example pgvector cosine similarity
127
+ * ```ts
128
+ * and(eq('userId', ctx.userId), raw('embedding <=> ? < 0.3', [queryEmbedding]))
129
+ * ```
130
+ */
131
+ interface FilterRaw {
132
+ readonly op: 'raw';
133
+ /** Kit-native query fragment. Embedded verbatim by each kit's compiler. */
134
+ readonly sql: string;
135
+ /** Positional params bound at the position of this fragment in the final query. */
136
+ readonly params?: readonly unknown[];
137
+ }
138
+ /** Root discriminated-union node. Every kit's compiler pattern-matches on `op`. */
139
+ type Filter = FilterEq | FilterNe | FilterGt | FilterGte | FilterLt | FilterLte | FilterIn | FilterNin | FilterLike | FilterRegex | FilterExists | FilterAnd | FilterOr | FilterNot | FilterTrue | FilterFalse | FilterRaw;
140
+ /** Narrow set of ops for pattern-matching exhaustiveness checks. */
141
+ type FilterOp = Filter['op'];
142
+ //#endregion
143
+ export { Filter, FilterAnd, FilterEq, FilterExists, FilterFalse, FilterGt, FilterGte, FilterIn, FilterLike, FilterLt, FilterLte, FilterNe, FilterNin, FilterNot, FilterOp, FilterOr, FilterRaw, FilterRegex, FilterTrue };
@@ -0,0 +1,24 @@
1
+ import { Filter } from "./types.mjs";
2
+
3
+ //#region src/filter/walk.d.ts
4
+ /**
5
+ * Depth-first visit of every node. Visitor returns `false` to stop descent
6
+ * into the current subtree (e.g. optimization prunes). Otherwise visits
7
+ * children.
8
+ */
9
+ declare function walkFilter(filter: Filter, visit: (node: Filter) => boolean | undefined): void;
10
+ /**
11
+ * Post-order transform. `transform` sees every node after its children have
12
+ * been rewritten, so leaf rewrites cascade up. Return the same node
13
+ * reference to opt-out of a rewrite at that level.
14
+ *
15
+ * Guarantees:
16
+ * - Immutable — never mutates input.
17
+ * - Identity-preserving — when no child changes, the parent node is
18
+ * returned unchanged (useful for structural sharing in caches).
19
+ */
20
+ declare function mapFilter(filter: Filter, transform: (node: Filter) => Filter): Filter;
21
+ /** Collect every field name referenced in the tree. Useful for index hints, policy checks. */
22
+ declare function collectFields(filter: Filter): string[];
23
+ //#endregion
24
+ export { collectFields, mapFilter, walkFilter };