@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/lib/clamp-int.d.ts +8 -0
- package/dist/lib/clamp-int.d.ts.map +1 -0
- package/dist/lib/clamp-int.js +15 -0
- package/dist/lib/filters/filter-exceptions.d.ts +92 -0
- package/dist/lib/filters/filter-exceptions.d.ts.map +1 -0
- package/dist/lib/filters/filter-exceptions.js +117 -0
- package/dist/lib/filters/negation.d.ts +33 -0
- package/dist/lib/filters/negation.d.ts.map +1 -0
- package/dist/lib/filters/negation.js +59 -0
- package/dist/lib/filters/operator-support.d.ts +25 -0
- package/dist/lib/filters/operator-support.d.ts.map +1 -0
- package/dist/lib/filters/operator-support.js +88 -0
- package/dist/lib/filters/own-property.d.ts +28 -0
- package/dist/lib/filters/own-property.d.ts.map +1 -0
- package/dist/lib/filters/own-property.js +34 -0
- package/dist/lib/filters/parse-filter-tree.d.ts +17 -0
- package/dist/lib/filters/parse-filter-tree.d.ts.map +1 -0
- package/dist/lib/filters/parse-filter-tree.js +107 -0
- package/dist/lib/filters/relation-exists.d.ts +35 -0
- package/dist/lib/filters/relation-exists.d.ts.map +1 -0
- package/dist/lib/filters/relation-exists.js +119 -0
- package/dist/lib/filters/resolve-leaf.d.ts +8 -0
- package/dist/lib/filters/resolve-leaf.d.ts.map +1 -0
- package/dist/lib/filters/resolve-leaf.js +170 -0
- package/dist/lib/filters/scalar-op.d.ts +14 -0
- package/dist/lib/filters/scalar-op.d.ts.map +1 -0
- package/dist/lib/filters/scalar-op.js +66 -0
- package/dist/lib/filters/table-helpers.d.ts +67 -0
- package/dist/lib/filters/table-helpers.d.ts.map +1 -0
- package/dist/lib/filters/table-helpers.js +68 -0
- package/dist/lib/filters/tree-to-drizzle.d.ts +35 -0
- package/dist/lib/filters/tree-to-drizzle.d.ts.map +1 -0
- package/dist/lib/filters/tree-to-drizzle.js +76 -0
- package/dist/lib/filters/types.d.ts +217 -0
- package/dist/lib/filters/types.d.ts.map +1 -0
- package/dist/lib/filters/types.js +35 -0
- package/dist/lib/pg-errors.d.ts +43 -0
- package/dist/lib/pg-errors.d.ts.map +1 -0
- package/dist/lib/pg-errors.js +82 -0
- package/package.json +35 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFilterTree = parseFilterTree;
|
|
4
|
+
const filter_exceptions_1 = require("./filter-exceptions");
|
|
5
|
+
const resolve_leaf_1 = require("./resolve-leaf");
|
|
6
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
7
|
+
const DEFAULT_MAX_NODES = 50;
|
|
8
|
+
const DEFAULT_MAX_GROUP_DEPTH = 5;
|
|
9
|
+
const DEFAULT_MAX_IN_LIST = 100;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a `filter` payload into a tree the translator can walk. Two
|
|
12
|
+
* input shapes are accepted:
|
|
13
|
+
*
|
|
14
|
+
* 1. `string` — a JSON-encoded tree, so a controller can forward
|
|
15
|
+
* `?filter=<json>` without pre-parsing. Throws `InvalidJson` on
|
|
16
|
+
* malformed input.
|
|
17
|
+
* 2. Object — already-parsed tree (`{ and: [...] }` / `{ or: [...] }` /
|
|
18
|
+
* single rule `{ field, op, value }`). Recursively validated; each
|
|
19
|
+
* leaf flows through the same schema/op/coercion check.
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` when the input is missing, empty, or `{}` so callers
|
|
22
|
+
* can skip the WHERE clause without an empty-array dance.
|
|
23
|
+
*/
|
|
24
|
+
function parseFilterTree(rawFilter, schema) {
|
|
25
|
+
if (rawFilter === undefined || rawFilter === null)
|
|
26
|
+
return null;
|
|
27
|
+
let value = rawFilter;
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (trimmed.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
value = JSON.parse(trimmed);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidJson, 'filter string is not valid JSON', { reason: err.message });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
40
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidShape, 'expected object');
|
|
41
|
+
}
|
|
42
|
+
const obj = value;
|
|
43
|
+
if (Object.keys(obj).length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const ctx = {
|
|
46
|
+
nodeCount: 0,
|
|
47
|
+
maxNodes: schema.maxNodes ?? DEFAULT_MAX_NODES,
|
|
48
|
+
maxGroupDepth: schema.maxGroupDepth ?? DEFAULT_MAX_GROUP_DEPTH,
|
|
49
|
+
maxDepth: schema.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
50
|
+
maxInListLength: schema.maxInListLength ?? DEFAULT_MAX_IN_LIST
|
|
51
|
+
};
|
|
52
|
+
return walkNode(obj, schema, 0, ctx);
|
|
53
|
+
}
|
|
54
|
+
function walkNode(raw, schema, depth, ctx) {
|
|
55
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
56
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'tree node must be an object');
|
|
57
|
+
}
|
|
58
|
+
ctx.nodeCount += 1;
|
|
59
|
+
if (ctx.nodeCount > ctx.maxNodes) {
|
|
60
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.MaxNodesExceeded, `filter exceeds max node count ${ctx.maxNodes}`, { maxNodes: ctx.maxNodes });
|
|
61
|
+
}
|
|
62
|
+
const node = raw;
|
|
63
|
+
// `Object.hasOwn`, not `in`: `parseFilterTree` also accepts an
|
|
64
|
+
// already-parsed object, which a caller could hand over with a prototype
|
|
65
|
+
// that carries these names. Shape detection must read what the payload
|
|
66
|
+
// itself declares.
|
|
67
|
+
const isAnd = Object.hasOwn(node, 'and');
|
|
68
|
+
const isOr = Object.hasOwn(node, 'or');
|
|
69
|
+
if (isAnd && isOr) {
|
|
70
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'group node must declare exactly one of `and` or `or`');
|
|
71
|
+
}
|
|
72
|
+
if (isAnd || isOr) {
|
|
73
|
+
if (depth >= ctx.maxGroupDepth) {
|
|
74
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.GroupDepthExceeded, `group nesting exceeds max depth ${ctx.maxGroupDepth}`, { maxGroupDepth: ctx.maxGroupDepth });
|
|
75
|
+
}
|
|
76
|
+
const combinator = isAnd ? 'and' : 'or';
|
|
77
|
+
const children = node[combinator];
|
|
78
|
+
if (!Array.isArray(children)) {
|
|
79
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, `\`${combinator}\` must be an array of nodes`);
|
|
80
|
+
}
|
|
81
|
+
if (children.length === 0) {
|
|
82
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, `\`${combinator}\` group must contain at least one child`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
kind: 'group',
|
|
86
|
+
combinator,
|
|
87
|
+
children: children.map((c) => walkNode(c, schema, depth + 1, ctx))
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (Object.hasOwn(node, 'field') && Object.hasOwn(node, 'op')) {
|
|
91
|
+
const field = node.field;
|
|
92
|
+
const op = node.op;
|
|
93
|
+
if (typeof field !== 'string' || field.length === 0) {
|
|
94
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'rule `field` must be a non-empty string');
|
|
95
|
+
}
|
|
96
|
+
if (typeof op !== 'string' || op.length === 0) {
|
|
97
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'rule `op` must be a non-empty string');
|
|
98
|
+
}
|
|
99
|
+
const path = field.split('.');
|
|
100
|
+
const leaf = (0, resolve_leaf_1.resolveLeaf)(path, op, node.value, schema, ctx.maxDepth, ctx.maxInListLength);
|
|
101
|
+
return toRule(leaf);
|
|
102
|
+
}
|
|
103
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'tree node must have `and`, `or`, or (`field` + `op`)');
|
|
104
|
+
}
|
|
105
|
+
function toRule(leaf) {
|
|
106
|
+
return { kind: 'rule', ...leaf };
|
|
107
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type SQL } from 'drizzle-orm';
|
|
2
|
+
import { type DbLike, type TableLike } from './table-helpers';
|
|
3
|
+
import type { ParsedFilter, RelationSchema } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Build an `EXISTS (...)` subquery that applies `inner` against the
|
|
6
|
+
* relation's target/through table. The `kind` discriminant selects the
|
|
7
|
+
* SQL shape:
|
|
8
|
+
*
|
|
9
|
+
* - `one-to-one` / `one-to-many`: target FK references parent.
|
|
10
|
+
* - `many-to-one`: parent FK references target.
|
|
11
|
+
* - `many-to-many`: join via `through`; add inner join to target when
|
|
12
|
+
* filtering on a target field.
|
|
13
|
+
* - `self-referential`: parent and target are the same physical table,
|
|
14
|
+
* so the target is aliased to give the subquery its own correlation
|
|
15
|
+
* name (an unaliased subquery would bind both correlation sides to the
|
|
16
|
+
* inner scope and silently degrade to "a row that is its own parent").
|
|
17
|
+
*
|
|
18
|
+
* Every branch ANDs the relation's optional `scope` predicate (workspace
|
|
19
|
+
* + soft-delete guard) inside the EXISTS, so a relation filter never
|
|
20
|
+
* traverses rows the root query itself excludes.
|
|
21
|
+
*
|
|
22
|
+
* Every column that lives on the PARENT side (`fk` for `many-to-one` /
|
|
23
|
+
* `self-referential`, and any `parentKey`) is re-resolved against `parent`
|
|
24
|
+
* via {@link rebind}, because `parent` may be an alias of the physical
|
|
25
|
+
* table — see that helper for why an unaliased column silently correlates
|
|
26
|
+
* to the wrong row.
|
|
27
|
+
*/
|
|
28
|
+
export declare function relationExists(rel: RelationSchema, parent: TableLike, inner: ParsedFilter, db: DbLike): SQL;
|
|
29
|
+
/**
|
|
30
|
+
* Apply the next segment of a filter path against `currentTable`.
|
|
31
|
+
* If the path has one segment left it resolves to a scalar condition;
|
|
32
|
+
* otherwise it descends through the next nested relation.
|
|
33
|
+
*/
|
|
34
|
+
export declare function descend(rel: RelationSchema, currentTable: TableLike, f: ParsedFilter, db: DbLike): SQL;
|
|
35
|
+
//# sourceMappingURL=relation-exists.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation-exists.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/relation-exists.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;AAM7D,OAAO,EAIH,KAAK,MAAM,EACX,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAC1B,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,YAAY,EACnB,EAAE,EAAE,MAAM,GACX,GAAG,CA6GL;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACnB,GAAG,EAAE,cAAc,EACnB,YAAY,EAAE,SAAS,EACvB,CAAC,EAAE,YAAY,EACf,EAAE,EAAE,MAAM,GACX,GAAG,CAeL"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.relationExists = relationExists;
|
|
4
|
+
exports.descend = descend;
|
|
5
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
6
|
+
const pg_core_1 = require("drizzle-orm/pg-core");
|
|
7
|
+
const filter_exceptions_1 = require("./filter-exceptions");
|
|
8
|
+
const own_property_1 = require("./own-property");
|
|
9
|
+
const scalar_op_1 = require("./scalar-op");
|
|
10
|
+
const table_helpers_1 = require("./table-helpers");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
/**
|
|
13
|
+
* Build an `EXISTS (...)` subquery that applies `inner` against the
|
|
14
|
+
* relation's target/through table. The `kind` discriminant selects the
|
|
15
|
+
* SQL shape:
|
|
16
|
+
*
|
|
17
|
+
* - `one-to-one` / `one-to-many`: target FK references parent.
|
|
18
|
+
* - `many-to-one`: parent FK references target.
|
|
19
|
+
* - `many-to-many`: join via `through`; add inner join to target when
|
|
20
|
+
* filtering on a target field.
|
|
21
|
+
* - `self-referential`: parent and target are the same physical table,
|
|
22
|
+
* so the target is aliased to give the subquery its own correlation
|
|
23
|
+
* name (an unaliased subquery would bind both correlation sides to the
|
|
24
|
+
* inner scope and silently degrade to "a row that is its own parent").
|
|
25
|
+
*
|
|
26
|
+
* Every branch ANDs the relation's optional `scope` predicate (workspace
|
|
27
|
+
* + soft-delete guard) inside the EXISTS, so a relation filter never
|
|
28
|
+
* traverses rows the root query itself excludes.
|
|
29
|
+
*
|
|
30
|
+
* Every column that lives on the PARENT side (`fk` for `many-to-one` /
|
|
31
|
+
* `self-referential`, and any `parentKey`) is re-resolved against `parent`
|
|
32
|
+
* via {@link rebind}, because `parent` may be an alias of the physical
|
|
33
|
+
* table — see that helper for why an unaliased column silently correlates
|
|
34
|
+
* to the wrong row.
|
|
35
|
+
*/
|
|
36
|
+
function relationExists(rel, parent, inner, db) {
|
|
37
|
+
switch (rel.kind) {
|
|
38
|
+
case types_1.RelationKind.OneToOne:
|
|
39
|
+
case types_1.RelationKind.OneToMany: {
|
|
40
|
+
const parentKey = rel.parentKey
|
|
41
|
+
? (0, table_helpers_1.rebind)(rel.parentKey, parent)
|
|
42
|
+
: (0, table_helpers_1.primaryKey)(parent);
|
|
43
|
+
const condition = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(rel.fk, parentKey), rel.scope?.(rel.table), descend(rel, rel.table, inner, db));
|
|
44
|
+
return (0, drizzle_orm_1.exists)(db
|
|
45
|
+
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
46
|
+
.from(rel.table)
|
|
47
|
+
.where(condition));
|
|
48
|
+
}
|
|
49
|
+
case types_1.RelationKind.ManyToOne: {
|
|
50
|
+
const targetKey = rel.targetKey ?? (0, table_helpers_1.primaryKey)(rel.table);
|
|
51
|
+
const condition = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(targetKey, (0, table_helpers_1.rebind)(rel.fk, parent)), rel.scope?.(rel.table), descend(rel, rel.table, inner, db));
|
|
52
|
+
return (0, drizzle_orm_1.exists)(db
|
|
53
|
+
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
54
|
+
.from(rel.table)
|
|
55
|
+
.where(condition));
|
|
56
|
+
}
|
|
57
|
+
case types_1.RelationKind.SelfReferential: {
|
|
58
|
+
// Parent and target are the same physical table, so an
|
|
59
|
+
// unaliased subquery would bind BOTH sides of the correlation
|
|
60
|
+
// to the inner scope. The alias gives the inner table its own
|
|
61
|
+
// correlation name; `rel.fk` stays bound to the OUTER row.
|
|
62
|
+
const target = (0, pg_core_1.alias)(rel.table, rel.alias);
|
|
63
|
+
const targetKey = (0, table_helpers_1.columnOf)(target, 'id');
|
|
64
|
+
const condition = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(targetKey, (0, table_helpers_1.rebind)(rel.fk, parent)), rel.scope?.(target), descend(rel, target, inner, db));
|
|
65
|
+
return (0, drizzle_orm_1.exists)(db
|
|
66
|
+
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
67
|
+
.from(target)
|
|
68
|
+
.where(condition));
|
|
69
|
+
}
|
|
70
|
+
case types_1.RelationKind.ManyToMany: {
|
|
71
|
+
const parentKey = rel.parentKey
|
|
72
|
+
? (0, table_helpers_1.rebind)(rel.parentKey, parent)
|
|
73
|
+
: (0, table_helpers_1.primaryKey)(parent);
|
|
74
|
+
// The junction-only fast path reads the target FK straight off
|
|
75
|
+
// the join row, skipping the target table. A `scope` lives on
|
|
76
|
+
// that target table, so it can only be enforced through the
|
|
77
|
+
// join — the fast path must yield to it, or `relation.id in (…)`
|
|
78
|
+
// would silently bypass the workspace / soft-delete guards that
|
|
79
|
+
// `relation.field` enforces.
|
|
80
|
+
const onlyTargetFk = !rel.scope && inner.path.length === 1 && inner.path[0] === 'id';
|
|
81
|
+
if (onlyTargetFk) {
|
|
82
|
+
return (0, drizzle_orm_1.exists)(db
|
|
83
|
+
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
84
|
+
.from(rel.through)
|
|
85
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(rel.fk, parentKey), (0, scalar_op_1.scalar)(rel.targetFk, inner.op, inner.value))));
|
|
86
|
+
}
|
|
87
|
+
if (!rel.table) {
|
|
88
|
+
throw new filter_exceptions_1.FilterSchemaException('many-to-many filter on target field requires `table`');
|
|
89
|
+
}
|
|
90
|
+
const targetKey = rel.targetKey ?? (0, table_helpers_1.primaryKey)(rel.table);
|
|
91
|
+
return (0, drizzle_orm_1.exists)(db
|
|
92
|
+
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
93
|
+
.from(rel.through)
|
|
94
|
+
.innerJoin(rel.table, (0, drizzle_orm_1.eq)(targetKey, rel.targetFk))
|
|
95
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(rel.fk, parentKey), rel.scope?.(rel.table), descend(rel, rel.table, inner, db))));
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
// Unreachable through the `RelationSchema` union, but a schema
|
|
99
|
+
// assembled at runtime can carry an unrecognised `kind`. Falling
|
|
100
|
+
// out of the switch returned `undefined`, and an `undefined`
|
|
101
|
+
// predicate is dropped by drizzle's `and()`/`or()` — the filter
|
|
102
|
+
// would silently widen instead of failing.
|
|
103
|
+
throw new filter_exceptions_1.FilterSchemaException(`relation declares unknown kind "${String(rel.kind)}"`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Apply the next segment of a filter path against `currentTable`.
|
|
108
|
+
* If the path has one segment left it resolves to a scalar condition;
|
|
109
|
+
* otherwise it descends through the next nested relation.
|
|
110
|
+
*/
|
|
111
|
+
function descend(rel, currentTable, f, db) {
|
|
112
|
+
if (f.path.length === 1) {
|
|
113
|
+
return (0, scalar_op_1.scalar)((0, table_helpers_1.columnOf)(currentTable, f.path[0]), f.op, f.value);
|
|
114
|
+
}
|
|
115
|
+
const next = (0, own_property_1.own)(rel.relations, f.path[0]);
|
|
116
|
+
if (!next)
|
|
117
|
+
throw new filter_exceptions_1.FilterSchemaException(`nested relation missing: ${f.path[0]}`);
|
|
118
|
+
return relationExists(next, currentTable, { ...f, path: f.path.slice(1) }, db);
|
|
119
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FilterSchema, ParsedFilter } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Walk a single dotted path against the schema, validate the operator,
|
|
4
|
+
* and coerce the value to the declared type. Produces the `ParsedFilter`
|
|
5
|
+
* leaf shape the tree parser tags with `kind: 'rule'`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveLeaf(path: string[], op: string, value: unknown, schema: FilterSchema, maxDepth: number, maxInListLength: number): ParsedFilter;
|
|
8
|
+
//# sourceMappingURL=resolve-leaf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-leaf.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/resolve-leaf.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAqB,MAAM,SAAS,CAAC;AAQ7E;;;;GAIG;AACH,wBAAgB,WAAW,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACxB,YAAY,CAkEd"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveLeaf = resolveLeaf;
|
|
4
|
+
const filter_exceptions_1 = require("./filter-exceptions");
|
|
5
|
+
const operator_support_1 = require("./operator-support");
|
|
6
|
+
const own_property_1 = require("./own-property");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const OPS = Object.values(types_1.FilterOperator);
|
|
9
|
+
/** Canonical 8-4-4-4-12 hex UUID — what Postgres' `uuid` type actually accepts. */
|
|
10
|
+
const UUID_CANONICAL = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
/**
|
|
12
|
+
* Walk a single dotted path against the schema, validate the operator,
|
|
13
|
+
* and coerce the value to the declared type. Produces the `ParsedFilter`
|
|
14
|
+
* leaf shape the tree parser tags with `kind: 'rule'`.
|
|
15
|
+
*/
|
|
16
|
+
function resolveLeaf(path, op, value, schema, maxDepth, maxInListLength) {
|
|
17
|
+
if (path.length === 0) {
|
|
18
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.EmptyPath, 'empty filter path');
|
|
19
|
+
}
|
|
20
|
+
if (path.length > maxDepth) {
|
|
21
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.DepthExceeded, `filter path exceeds max depth ${maxDepth}`, { path: path.join('.'), maxDepth });
|
|
22
|
+
}
|
|
23
|
+
if (!OPS.includes(op)) {
|
|
24
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.UnknownOperator, `unknown operator "${op}"`, { op });
|
|
25
|
+
}
|
|
26
|
+
let fields = schema.fields ?? {};
|
|
27
|
+
let relations = schema.relations ?? {};
|
|
28
|
+
for (let i = 0; i < path.length; i++) {
|
|
29
|
+
const seg = path[i];
|
|
30
|
+
const last = i === path.length - 1;
|
|
31
|
+
if (last) {
|
|
32
|
+
const field = (0, own_property_1.own)(fields, seg);
|
|
33
|
+
if (!field) {
|
|
34
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.UnknownField, `unknown field "${path.join('.')}"`, { path: path.join('.') });
|
|
35
|
+
}
|
|
36
|
+
assertOperatorAllowed(field, op, path);
|
|
37
|
+
return {
|
|
38
|
+
path,
|
|
39
|
+
op: op,
|
|
40
|
+
value: coerce(value, field, op, path, maxInListLength)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const rel = (0, own_property_1.own)(relations, seg);
|
|
44
|
+
if (!rel) {
|
|
45
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.UnknownRelation, `unknown relation "${seg}" in "${path.join('.')}"`, { segment: seg, path: path.join('.') });
|
|
46
|
+
}
|
|
47
|
+
fields = rel.fields ?? {};
|
|
48
|
+
relations = rel.relations ?? {};
|
|
49
|
+
}
|
|
50
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidShape, 'unreachable: path walker ended without a leaf');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reject an operator the field's column type cannot answer.
|
|
54
|
+
*
|
|
55
|
+
* Runs **before** coercion, so the reported issue is the operator rather than a
|
|
56
|
+
* value the operator would never have accepted anyway — `embargoUntil ilike
|
|
57
|
+
* "%2020%"` should say "ilike is not available on a date field", not "not a
|
|
58
|
+
* date: %2020%". The context carries `path`, `op` and the `allowed` list, so a
|
|
59
|
+
* client can render a per-field issue and offer the operators that do work.
|
|
60
|
+
*
|
|
61
|
+
* An unrecognised declared type is left alone: that is a **schema** bug, and
|
|
62
|
+
* `scalarOf` already raises the 500 (`FilterSchemaException`) that names it.
|
|
63
|
+
* Blaming the client with a 400 here would hide it.
|
|
64
|
+
*/
|
|
65
|
+
function assertOperatorAllowed(field, op, path) {
|
|
66
|
+
const allowed = (0, operator_support_1.operatorsFor)(field.type);
|
|
67
|
+
if (!allowed || allowed.includes(op)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.OperatorNotAllowed, `operator "${op}" is not available on "${path.join('.')}" (a ${field.type} field)`, { path: path.join('.'), op, fieldType: field.type, allowed });
|
|
71
|
+
}
|
|
72
|
+
function coerce(raw, field, op, path, maxInListLength) {
|
|
73
|
+
const pathStr = path.join('.');
|
|
74
|
+
if (op === types_1.FilterOperator.Null) {
|
|
75
|
+
if (raw === 'true' || raw === true)
|
|
76
|
+
return true;
|
|
77
|
+
if (raw === 'false' || raw === false)
|
|
78
|
+
return false;
|
|
79
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, 'null filter accepts only true|false', { path: pathStr, op, value: raw });
|
|
80
|
+
}
|
|
81
|
+
if (op === types_1.FilterOperator.In || op === types_1.FilterOperator.Nin) {
|
|
82
|
+
const items = Array.isArray(raw)
|
|
83
|
+
? raw
|
|
84
|
+
: typeof raw === 'string'
|
|
85
|
+
? raw.split(',')
|
|
86
|
+
: [raw];
|
|
87
|
+
// An empty list would translate to `IN ()` / `NOT IN ()`, which
|
|
88
|
+
// Drizzle emits as `false` / `true` — a silent no-op (matches
|
|
89
|
+
// nothing) or inverted filter (matches everything). Reject it so
|
|
90
|
+
// the client gets a clean 400 instead of a surprising result set.
|
|
91
|
+
if (items.length === 0) {
|
|
92
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.EmptyInList, `${op} requires at least one value`, { path: pathStr, op });
|
|
93
|
+
}
|
|
94
|
+
// Cap the list so a single rule (one node, under maxNodes) can't
|
|
95
|
+
// blow up into an arbitrarily large IN clause.
|
|
96
|
+
if (items.length > maxInListLength) {
|
|
97
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.MaxInListExceeded, `${op} value list exceeds max length ${maxInListLength}`, { path: pathStr, op, maxInListLength, length: items.length });
|
|
98
|
+
}
|
|
99
|
+
return items.map((v) => scalarOf(v, field, pathStr));
|
|
100
|
+
}
|
|
101
|
+
return scalarOf(raw, field, pathStr);
|
|
102
|
+
}
|
|
103
|
+
function scalarOf(v, field, pathStr) {
|
|
104
|
+
// A filter value has to be a scalar. `String(v)` on anything else produces
|
|
105
|
+
// a plausible-looking string that is then MATCHED AGAINST rather than
|
|
106
|
+
// rejected: `null` → `"null"`, a missing `value` key → `"undefined"`,
|
|
107
|
+
// `{}` → `"[object Object]"`, `[1,2]` → `"1,2"`, and (for a number field)
|
|
108
|
+
// `[]` → `""` → `0`. Each returns 200 with a wrong, usually empty, result
|
|
109
|
+
// set that the client reads as "no matches" instead of "bad request" —
|
|
110
|
+
// the one silent failure mode in a library that 400s every other bad
|
|
111
|
+
// value. A JSON client meaning "is null" wants the `null` operator.
|
|
112
|
+
if (v === null ||
|
|
113
|
+
(typeof v !== 'string' &&
|
|
114
|
+
typeof v !== 'number' &&
|
|
115
|
+
typeof v !== 'boolean')) {
|
|
116
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, v === undefined
|
|
117
|
+
? 'value is required'
|
|
118
|
+
: 'value must be a string, number or boolean', { path: pathStr, expectedType: field.type });
|
|
119
|
+
}
|
|
120
|
+
const s = typeof v === 'string' ? v : String(v);
|
|
121
|
+
switch (field.type) {
|
|
122
|
+
case types_1.ScalarFieldType.String:
|
|
123
|
+
return s;
|
|
124
|
+
case types_1.ScalarFieldType.Number: {
|
|
125
|
+
const n = Number(s);
|
|
126
|
+
if (!Number.isFinite(n)) {
|
|
127
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `not a number: ${s}`, { path: pathStr, expectedType: 'number', value: s });
|
|
128
|
+
}
|
|
129
|
+
return n;
|
|
130
|
+
}
|
|
131
|
+
case types_1.ScalarFieldType.Boolean:
|
|
132
|
+
if (s === 'true')
|
|
133
|
+
return true;
|
|
134
|
+
if (s === 'false')
|
|
135
|
+
return false;
|
|
136
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `not a boolean: ${s}`, { path: pathStr, expectedType: 'boolean', value: s });
|
|
137
|
+
case types_1.ScalarFieldType.Uuid:
|
|
138
|
+
// Canonical 8-4-4-4-12 form — stricter than a loose `[0-9a-f-]{36}`,
|
|
139
|
+
// which would let malformed values (e.g. 36 dashes) pass coercion
|
|
140
|
+
// and then fail Postgres' uuid cast as a 500 instead of a clean 400.
|
|
141
|
+
if (!UUID_CANONICAL.test(s)) {
|
|
142
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `not a uuid: ${s}`, { path: pathStr, expectedType: 'uuid', value: s });
|
|
143
|
+
}
|
|
144
|
+
return s;
|
|
145
|
+
case types_1.ScalarFieldType.Date: {
|
|
146
|
+
const d = new Date(s);
|
|
147
|
+
if (Number.isNaN(d.getTime())) {
|
|
148
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `not a date: ${s}`, { path: pathStr, expectedType: 'date', value: s });
|
|
149
|
+
}
|
|
150
|
+
return d;
|
|
151
|
+
}
|
|
152
|
+
case types_1.ScalarFieldType.Enum:
|
|
153
|
+
if (!field.enumValues?.includes(s)) {
|
|
154
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `not in enum: ${s}`, {
|
|
155
|
+
path: pathStr,
|
|
156
|
+
expectedType: 'enum',
|
|
157
|
+
value: s,
|
|
158
|
+
allowed: field.enumValues
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return s;
|
|
162
|
+
default:
|
|
163
|
+
// Not reachable through the type system, but a schema built at
|
|
164
|
+
// runtime (the content plugin derives one per content type) can
|
|
165
|
+
// land here with an unrecognised `type`. Falling out of the switch
|
|
166
|
+
// returned `undefined`, which drizzle renders as the same broken
|
|
167
|
+
// `$1 = ` fragment an inherited field name used to produce.
|
|
168
|
+
throw new filter_exceptions_1.FilterSchemaException(`field "${pathStr}" declares unknown type "${String(field.type)}"`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AnyColumn, type SQL } from 'drizzle-orm';
|
|
2
|
+
import { FilterOperator } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Translate a single operator + value pair against `col` into a Drizzle
|
|
5
|
+
* SQL fragment. Array-valued ops (`in`/`nin`) expect the caller's value
|
|
6
|
+
* to already be an array; `null` expects a boolean.
|
|
7
|
+
*
|
|
8
|
+
* Negative operators are NULL-inclusive — see {@link negative}. (On a
|
|
9
|
+
* relation path a negative operator never reaches here: the translator
|
|
10
|
+
* rewrites it into `NOT EXISTS(… positive …)` first, which is the only
|
|
11
|
+
* correct reading once the relation can hold more than one row.)
|
|
12
|
+
*/
|
|
13
|
+
export declare function scalar(col: AnyColumn, op: FilterOperator, value: unknown): SQL;
|
|
14
|
+
//# sourceMappingURL=scalar-op.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scalar-op.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/scalar-op.ts"],"names":[],"mappings":"AAAA,OAAO,EAeH,KAAK,SAAS,EACd,KAAK,GAAG,EACX,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAiBzC;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAClB,GAAG,EAAE,SAAS,EACd,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,OAAO,GACf,GAAG,CAqCL"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scalar = scalar;
|
|
4
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
5
|
+
const filter_exceptions_1 = require("./filter-exceptions");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
/**
|
|
8
|
+
* A negative predicate that also matches NULL.
|
|
9
|
+
*
|
|
10
|
+
* SQL's three-valued logic makes `col <> 'x'`, `col NOT IN (…)` and
|
|
11
|
+
* `col NOT ILIKE '%x%'` all evaluate to NULL — i.e. *not matched* — when
|
|
12
|
+
* the column is NULL. That reads as a silent data-loss bug to an editor:
|
|
13
|
+
* a publishable type keeps its required fields **nullable** (they're only
|
|
14
|
+
* required to publish), so "Title does not contain foo" would quietly hide
|
|
15
|
+
* every draft whose title is still empty. An empty value is not the value
|
|
16
|
+
* being excluded, so it belongs in the result.
|
|
17
|
+
*/
|
|
18
|
+
function negative(col, predicate) {
|
|
19
|
+
return (0, drizzle_orm_1.or)(predicate, (0, drizzle_orm_1.isNull)(col));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Translate a single operator + value pair against `col` into a Drizzle
|
|
23
|
+
* SQL fragment. Array-valued ops (`in`/`nin`) expect the caller's value
|
|
24
|
+
* to already be an array; `null` expects a boolean.
|
|
25
|
+
*
|
|
26
|
+
* Negative operators are NULL-inclusive — see {@link negative}. (On a
|
|
27
|
+
* relation path a negative operator never reaches here: the translator
|
|
28
|
+
* rewrites it into `NOT EXISTS(… positive …)` first, which is the only
|
|
29
|
+
* correct reading once the relation can hold more than one row.)
|
|
30
|
+
*/
|
|
31
|
+
function scalar(col, op, value) {
|
|
32
|
+
switch (op) {
|
|
33
|
+
case types_1.FilterOperator.Eq:
|
|
34
|
+
return (0, drizzle_orm_1.eq)(col, value);
|
|
35
|
+
case types_1.FilterOperator.Ne:
|
|
36
|
+
return negative(col, (0, drizzle_orm_1.ne)(col, value));
|
|
37
|
+
case types_1.FilterOperator.Gt:
|
|
38
|
+
return (0, drizzle_orm_1.gt)(col, value);
|
|
39
|
+
case types_1.FilterOperator.Gte:
|
|
40
|
+
return (0, drizzle_orm_1.gte)(col, value);
|
|
41
|
+
case types_1.FilterOperator.Lt:
|
|
42
|
+
return (0, drizzle_orm_1.lt)(col, value);
|
|
43
|
+
case types_1.FilterOperator.Lte:
|
|
44
|
+
return (0, drizzle_orm_1.lte)(col, value);
|
|
45
|
+
case types_1.FilterOperator.In:
|
|
46
|
+
return (0, drizzle_orm_1.inArray)(col, value);
|
|
47
|
+
case types_1.FilterOperator.Nin:
|
|
48
|
+
return negative(col, (0, drizzle_orm_1.notInArray)(col, value));
|
|
49
|
+
case types_1.FilterOperator.Like:
|
|
50
|
+
return (0, drizzle_orm_1.like)(col, String(value));
|
|
51
|
+
case types_1.FilterOperator.Ilike:
|
|
52
|
+
return (0, drizzle_orm_1.ilike)(col, String(value));
|
|
53
|
+
case types_1.FilterOperator.Nilike:
|
|
54
|
+
return negative(col, (0, drizzle_orm_1.notIlike)(col, String(value)));
|
|
55
|
+
case types_1.FilterOperator.Null:
|
|
56
|
+
return value === true ? (0, drizzle_orm_1.isNull)(col) : (0, drizzle_orm_1.isNotNull)(col);
|
|
57
|
+
default:
|
|
58
|
+
// The parser validates `op` against the vocabulary, so this is
|
|
59
|
+
// unreachable — but falling out of the switch returned `undefined`,
|
|
60
|
+
// and drizzle's `and()`/`or()` drop an undefined member, so a
|
|
61
|
+
// future operator added to `FilterOperator` without a case here
|
|
62
|
+
// would silently widen the filter instead of failing the build's
|
|
63
|
+
// intent. Fail loudly instead.
|
|
64
|
+
throw new filter_exceptions_1.FilterSchemaException(`no translation for operator "${String(op)}"`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type AnyColumn, type SQLWrapper } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* A minimal Drizzle-like table shape.
|
|
4
|
+
*
|
|
5
|
+
* We deliberately do NOT import drizzle-orm's `Table`/`PgTable` here:
|
|
6
|
+
* drizzle's generic `Column`/`TableConfig` use a protected field that
|
|
7
|
+
* breaks structural assignment between utils-server and callers'
|
|
8
|
+
* concrete `PgTableWithColumns<…>` types under nodenext resolution.
|
|
9
|
+
* The parser is the runtime whitelist that keeps this safe.
|
|
10
|
+
*/
|
|
11
|
+
export type TableLike = object;
|
|
12
|
+
/**
|
|
13
|
+
* A minimal Drizzle-like database shape. Only `.select().from()` and
|
|
14
|
+
* `.innerJoin()` are used; parameter/return types are widened so
|
|
15
|
+
* concrete Drizzle instances (e.g. `NodePgDatabase`) assign here
|
|
16
|
+
* without cross-package nominal friction.
|
|
17
|
+
*/
|
|
18
|
+
export type DbLike = {
|
|
19
|
+
select: (...args: any[]) => {
|
|
20
|
+
from: (t: any) => {
|
|
21
|
+
where: (cond: any) => SQLWrapper;
|
|
22
|
+
innerJoin: (t: any, cond: any) => {
|
|
23
|
+
where: (cond: any) => SQLWrapper;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a column on a table by name, or throw if absent.
|
|
30
|
+
*
|
|
31
|
+
* The lookup is `Object.hasOwn`-guarded: drizzle's column map is a plain
|
|
32
|
+
* object, so `cols['constructor']` would otherwise return `Object` itself and
|
|
33
|
+
* this function would hand a `Function` to drizzle as a `Column`. The parser's
|
|
34
|
+
* whitelist is the primary guard, but this is the site where an escaped name
|
|
35
|
+
* turns into malformed SQL rather than an error, so it guards itself too.
|
|
36
|
+
*/
|
|
37
|
+
export declare function columnOf(table: TableLike, name: string): AnyColumn;
|
|
38
|
+
/**
|
|
39
|
+
* Re-resolve a prebuilt column against the table the translator is actually
|
|
40
|
+
* querying.
|
|
41
|
+
*
|
|
42
|
+
* A `RelationSchema`'s parent-side columns (`fk` on a `many-to-one` /
|
|
43
|
+
* `self-referential`, and every `parentKey`) are built once, against the
|
|
44
|
+
* **physical** parent table. That is wrong the moment the parent is an
|
|
45
|
+
* *alias* — which happens for every relation nested under a
|
|
46
|
+
* `self-referential` hop, whose subquery is `FROM t AS qb_x`. An unaliased
|
|
47
|
+
* `t.col` inside that subquery is still resolvable from the OUTERMOST
|
|
48
|
+
* query's `FROM t`, so Postgres silently binds the correlation to the root
|
|
49
|
+
* row instead of the aliased one: `parent.author.name` would filter on the
|
|
50
|
+
* ROOT row's author, and `parent.parent.name` would collapse to
|
|
51
|
+
* `parent.name`. Valid SQL, wrong rows, no error.
|
|
52
|
+
*
|
|
53
|
+
* Rebinding by the column's own DB name against the queried table fixes the
|
|
54
|
+
* correlation. It is a no-op when the parent is the physical table (the
|
|
55
|
+
* lookup finds the identical column), so unaliased paths are unaffected.
|
|
56
|
+
* Only ever call this for columns that live on the parent **by
|
|
57
|
+
* construction** — never for a target-side `fk` (`one-to-many`), where the
|
|
58
|
+
* name could collide with an unrelated parent column.
|
|
59
|
+
*/
|
|
60
|
+
export declare function rebind(column: AnyColumn, table: TableLike): AnyColumn;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a table's primary key (defaults to an `id` column).
|
|
63
|
+
* Callers can override with an explicit `parentKey`/`targetKey` in the
|
|
64
|
+
* relation schema when the table's primary key is named differently.
|
|
65
|
+
*/
|
|
66
|
+
export declare function primaryKey(table: TableLike): AnyColumn;
|
|
67
|
+
//# sourceMappingURL=table-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAG/E;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAG/B;;;;;GAKG;AACH,MAAM,MAAM,MAAM,GAAG;IACjB,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK;QACxB,IAAI,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK;YACd,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,UAAU,CAAC;YACjC,SAAS,EAAE,CACP,CAAC,EAAE,GAAG,EACN,IAAI,EAAE,GAAG,KACR;gBACD,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,UAAU,CAAC;aACpC,CAAC;SACL,CAAC;KACL,CAAC;CACL,CAAC;AAGF;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAMlE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,CAOrE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAOtD"}
|