@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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/cache/index.d.mts +4 -0
- package/dist/cache/index.mjs +3 -0
- package/dist/cache/memory-adapter.d.mts +7 -0
- package/dist/cache/memory-adapter.mjs +37 -0
- package/dist/cache/stable-stringify.d.mts +15 -0
- package/dist/cache/stable-stringify.mjs +19 -0
- package/dist/cache/types.d.mts +59 -0
- package/dist/context/index.d.mts +2 -0
- package/dist/context/index.mjs +0 -0
- package/dist/context/types.d.mts +24 -0
- package/dist/errors/create-error.d.mts +19 -0
- package/dist/errors/create-error.mjs +23 -0
- package/dist/errors/duplicate-key.d.mts +38 -0
- package/dist/errors/duplicate-key.mjs +57 -0
- package/dist/errors/index.d.mts +4 -0
- package/dist/errors/index.mjs +3 -0
- package/dist/errors/types.d.mts +37 -0
- package/dist/filter/builders.d.mts +60 -0
- package/dist/filter/builders.mjs +172 -0
- package/dist/filter/guard.d.mts +13 -0
- package/dist/filter/guard.mjs +34 -0
- package/dist/filter/index.d.mts +7 -0
- package/dist/filter/index.mjs +6 -0
- package/dist/filter/match.d.mts +12 -0
- package/dist/filter/match.mjs +91 -0
- package/dist/filter/scope.d.mts +31 -0
- package/dist/filter/scope.mjs +54 -0
- package/dist/filter/types.d.mts +143 -0
- package/dist/filter/walk.d.mts +24 -0
- package/dist/filter/walk.mjs +77 -0
- package/dist/hooks/engine.d.mts +48 -0
- package/dist/hooks/engine.mjs +101 -0
- package/dist/hooks/events.d.mts +95 -0
- package/dist/hooks/events.mjs +93 -0
- package/dist/hooks/index.d.mts +5 -0
- package/dist/hooks/index.mjs +4 -0
- package/dist/hooks/priority.d.mts +23 -0
- package/dist/hooks/priority.mjs +21 -0
- package/dist/hooks/types.d.mts +37 -0
- package/dist/lookup/index.d.mts +2 -0
- package/dist/lookup/index.mjs +0 -0
- package/dist/lookup/types.d.mts +170 -0
- package/dist/operations/index.d.mts +3 -0
- package/dist/operations/index.mjs +2 -0
- package/dist/operations/registry.d.mts +41 -0
- package/dist/operations/registry.mjs +140 -0
- package/dist/operations/types.d.mts +49 -0
- package/dist/pagination/cursor.d.mts +44 -0
- package/dist/pagination/cursor.mjs +150 -0
- package/dist/pagination/index.d.mts +5 -0
- package/dist/pagination/index.mjs +4 -0
- package/dist/pagination/keyset.d.mts +25 -0
- package/dist/pagination/keyset.mjs +61 -0
- package/dist/pagination/offset.d.mts +26 -0
- package/dist/pagination/offset.mjs +47 -0
- package/dist/pagination/types.d.mts +136 -0
- package/dist/query-parser/coerce.d.mts +16 -0
- package/dist/query-parser/coerce.mjs +73 -0
- package/dist/query-parser/index.d.mts +4 -0
- package/dist/query-parser/index.mjs +3 -0
- package/dist/query-parser/parse-url.d.mts +7 -0
- package/dist/query-parser/parse-url.mjs +224 -0
- package/dist/query-parser/types.d.mts +104 -0
- package/dist/repository/base.d.mts +90 -0
- package/dist/repository/base.mjs +111 -0
- package/dist/repository/index.d.mts +5 -0
- package/dist/repository/index.mjs +3 -0
- package/dist/repository/plugin-types.d.mts +27 -0
- package/dist/repository/plugin-types.mjs +45 -0
- package/dist/repository/types.d.mts +470 -0
- package/dist/schema/field-rules.d.mts +62 -0
- package/dist/schema/field-rules.mjs +110 -0
- package/dist/schema/index.d.mts +3 -0
- package/dist/schema/index.mjs +2 -0
- package/dist/schema/types.d.mts +138 -0
- package/dist/testing/conformance.d.mts +6 -0
- package/dist/testing/conformance.mjs +481 -0
- package/dist/testing/index.d.mts +3 -0
- package/dist/testing/index.mjs +2 -0
- package/dist/testing/types.d.mts +113 -0
- 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 };
|