@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ortha CMS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { parseFilterTree } from './lib/filters/parse-filter-tree';
|
|
2
|
+
export { applyFilterTree, type ApplyFilterTreeOptions, type FilterExtensionResolver } from './lib/filters/tree-to-drizzle';
|
|
3
|
+
export type { DbLike, TableLike } from './lib/filters/table-helpers';
|
|
4
|
+
export { FilterOperator, ScalarFieldType, RelationKind } from './lib/filters/types';
|
|
5
|
+
export { OPERATORS_BY_TYPE, operatorsFor } from './lib/filters/operator-support';
|
|
6
|
+
export type { ScalarFieldSchema, FieldSchema, RelationSchema, RelationScope, FilterSchema, ParsedRule, ParsedGroup, ParsedNode } from './lib/filters/types';
|
|
7
|
+
export { FilterException, FilterErrorCode, FilterSchemaException } from './lib/filters/filter-exceptions';
|
|
8
|
+
export { clampInt } from './lib/clamp-int';
|
|
9
|
+
export { isForeignKeyViolation, isUniqueViolation, violatedConstraint } from './lib/pg-errors';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EACH,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC/B,MAAM,+BAA+B,CAAC;AACvC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EACH,cAAc,EACd,eAAe,EACf,YAAY,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,iBAAiB,EACjB,YAAY,EACf,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACR,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,aAAa,EACb,YAAY,EACZ,UAAU,EACV,WAAW,EACX,UAAU,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,eAAe,EACf,eAAe,EACf,qBAAqB,EACxB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EACH,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EACrB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.violatedConstraint = exports.isUniqueViolation = exports.isForeignKeyViolation = exports.clampInt = exports.FilterSchemaException = exports.FilterErrorCode = exports.FilterException = exports.operatorsFor = exports.OPERATORS_BY_TYPE = exports.RelationKind = exports.ScalarFieldType = exports.FilterOperator = exports.applyFilterTree = exports.parseFilterTree = void 0;
|
|
4
|
+
var parse_filter_tree_1 = require("./lib/filters/parse-filter-tree");
|
|
5
|
+
Object.defineProperty(exports, "parseFilterTree", { enumerable: true, get: function () { return parse_filter_tree_1.parseFilterTree; } });
|
|
6
|
+
var tree_to_drizzle_1 = require("./lib/filters/tree-to-drizzle");
|
|
7
|
+
Object.defineProperty(exports, "applyFilterTree", { enumerable: true, get: function () { return tree_to_drizzle_1.applyFilterTree; } });
|
|
8
|
+
var types_1 = require("./lib/filters/types");
|
|
9
|
+
Object.defineProperty(exports, "FilterOperator", { enumerable: true, get: function () { return types_1.FilterOperator; } });
|
|
10
|
+
Object.defineProperty(exports, "ScalarFieldType", { enumerable: true, get: function () { return types_1.ScalarFieldType; } });
|
|
11
|
+
Object.defineProperty(exports, "RelationKind", { enumerable: true, get: function () { return types_1.RelationKind; } });
|
|
12
|
+
var operator_support_1 = require("./lib/filters/operator-support");
|
|
13
|
+
Object.defineProperty(exports, "OPERATORS_BY_TYPE", { enumerable: true, get: function () { return operator_support_1.OPERATORS_BY_TYPE; } });
|
|
14
|
+
Object.defineProperty(exports, "operatorsFor", { enumerable: true, get: function () { return operator_support_1.operatorsFor; } });
|
|
15
|
+
var filter_exceptions_1 = require("./lib/filters/filter-exceptions");
|
|
16
|
+
Object.defineProperty(exports, "FilterException", { enumerable: true, get: function () { return filter_exceptions_1.FilterException; } });
|
|
17
|
+
Object.defineProperty(exports, "FilterErrorCode", { enumerable: true, get: function () { return filter_exceptions_1.FilterErrorCode; } });
|
|
18
|
+
Object.defineProperty(exports, "FilterSchemaException", { enumerable: true, get: function () { return filter_exceptions_1.FilterSchemaException; } });
|
|
19
|
+
var clamp_int_1 = require("./lib/clamp-int");
|
|
20
|
+
Object.defineProperty(exports, "clampInt", { enumerable: true, get: function () { return clamp_int_1.clampInt; } });
|
|
21
|
+
var pg_errors_1 = require("./lib/pg-errors");
|
|
22
|
+
Object.defineProperty(exports, "isForeignKeyViolation", { enumerable: true, get: function () { return pg_errors_1.isForeignKeyViolation; } });
|
|
23
|
+
Object.defineProperty(exports, "isUniqueViolation", { enumerable: true, get: function () { return pg_errors_1.isUniqueViolation; } });
|
|
24
|
+
Object.defineProperty(exports, "violatedConstraint", { enumerable: true, get: function () { return pg_errors_1.violatedConstraint; } });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a query-string integer with a default and inclusive bounds. A missing or
|
|
3
|
+
* non-numeric `raw` falls back to `fallback`; a numeric one is truncated and
|
|
4
|
+
* clamped into `[min, maxValue]`. Handy for pagination params (`?page=`,
|
|
5
|
+
* `?pageSize=`) where the caller wants a safe integer regardless of input.
|
|
6
|
+
*/
|
|
7
|
+
export declare function clampInt(raw: string | undefined, fallback: number, min: number, maxValue: number): number;
|
|
8
|
+
//# sourceMappingURL=clamp-int.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clamp-int.d.ts","sourceRoot":"","sources":["../../src/lib/clamp-int.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,QAAQ,CACpB,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,GACjB,MAAM,CAIR"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clampInt = clampInt;
|
|
4
|
+
/**
|
|
5
|
+
* Parse a query-string integer with a default and inclusive bounds. A missing or
|
|
6
|
+
* non-numeric `raw` falls back to `fallback`; a numeric one is truncated and
|
|
7
|
+
* clamped into `[min, maxValue]`. Handy for pagination params (`?page=`,
|
|
8
|
+
* `?pageSize=`) where the caller wants a safe integer regardless of input.
|
|
9
|
+
*/
|
|
10
|
+
function clampInt(raw, fallback, min, maxValue) {
|
|
11
|
+
const parsed = Number(raw);
|
|
12
|
+
if (!Number.isFinite(parsed))
|
|
13
|
+
return fallback;
|
|
14
|
+
return Math.min(maxValue, Math.max(min, Math.trunc(parsed)));
|
|
15
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
|
|
2
|
+
/** Machine-readable codes for filter parse errors. */
|
|
3
|
+
export declare const FilterErrorCode: {
|
|
4
|
+
/** `filter` was not an object (e.g. a string or array). */
|
|
5
|
+
readonly InvalidShape: "FILTER_INVALID_SHAPE";
|
|
6
|
+
/** Parser reached a leaf with no path segments. */
|
|
7
|
+
readonly EmptyPath: "FILTER_EMPTY_PATH";
|
|
8
|
+
/** Dotted path longer than `schema.maxDepth`. */
|
|
9
|
+
readonly DepthExceeded: "FILTER_DEPTH_EXCEEDED";
|
|
10
|
+
/** Operator not in the global operator vocabulary. */
|
|
11
|
+
readonly UnknownOperator: "FILTER_UNKNOWN_OPERATOR";
|
|
12
|
+
/**
|
|
13
|
+
* Operator is a real one, but not one the field's declared type can be
|
|
14
|
+
* asked — e.g. `ilike` against a `date`, a `number`, a `boolean` or a
|
|
15
|
+
* `uuid`. Postgres has no `~~` for those column types, so without this the
|
|
16
|
+
* rule reached the driver and came back as an unhandled 500 instead of a
|
|
17
|
+
* per-field issue.
|
|
18
|
+
*/
|
|
19
|
+
readonly OperatorNotAllowed: "FILTER_OPERATOR_NOT_ALLOWED";
|
|
20
|
+
/** Final segment names a field not declared on the schema. */
|
|
21
|
+
readonly UnknownField: "FILTER_UNKNOWN_FIELD";
|
|
22
|
+
/** Mid-path segment names a relation not declared on the schema. */
|
|
23
|
+
readonly UnknownRelation: "FILTER_UNKNOWN_RELATION";
|
|
24
|
+
/** Value failed type coercion (uuid/number/boolean/date/enum/null). */
|
|
25
|
+
readonly InvalidValue: "FILTER_INVALID_VALUE";
|
|
26
|
+
/** `filter` was a string but did not parse as JSON. */
|
|
27
|
+
readonly InvalidJson: "FILTER_INVALID_JSON";
|
|
28
|
+
/** Tree node was neither a recognised group nor a rule. */
|
|
29
|
+
readonly InvalidNode: "FILTER_INVALID_NODE";
|
|
30
|
+
/** Tree exceeded `schema.maxNodes` (default 50). */
|
|
31
|
+
readonly MaxNodesExceeded: "FILTER_MAX_NODES_EXCEEDED";
|
|
32
|
+
/** Group nesting exceeded `schema.maxGroupDepth` (default 5). */
|
|
33
|
+
readonly GroupDepthExceeded: "FILTER_GROUP_DEPTH_EXCEEDED";
|
|
34
|
+
/**
|
|
35
|
+
* `in`/`nin` value list was empty. Rejected because an empty `in`
|
|
36
|
+
* matches no rows and an empty `nin` matches every row — a silent
|
|
37
|
+
* no-op/inverted filter rather than the user's intent.
|
|
38
|
+
*/
|
|
39
|
+
readonly EmptyInList: "FILTER_EMPTY_IN_LIST";
|
|
40
|
+
/** `in`/`nin` value list exceeded `schema.maxInListLength` (default 100). */
|
|
41
|
+
readonly MaxInListExceeded: "FILTER_MAX_IN_LIST_EXCEEDED";
|
|
42
|
+
};
|
|
43
|
+
/** One of the {@link FilterErrorCode} values. */
|
|
44
|
+
export type FilterErrorCode = (typeof FilterErrorCode)[keyof typeof FilterErrorCode];
|
|
45
|
+
/**
|
|
46
|
+
* Typed 400 exception thrown by the filter parser.
|
|
47
|
+
*
|
|
48
|
+
* Still a `BadRequestException` under the hood so NestJS returns HTTP 400,
|
|
49
|
+
* but the response body carries a `code` (from {@link FilterErrorCode}) and
|
|
50
|
+
* a `context` object so clients can key off the category rather than
|
|
51
|
+
* string-matching the message.
|
|
52
|
+
*
|
|
53
|
+
* Example response body:
|
|
54
|
+
* ```json
|
|
55
|
+
* {
|
|
56
|
+
* "statusCode": 400,
|
|
57
|
+
* "error": "Bad Request",
|
|
58
|
+
* "code": "FILTER_UNKNOWN_FIELD",
|
|
59
|
+
* "message": "filter: unknown field \"secretField\"",
|
|
60
|
+
* "path": "secretField"
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare class FilterException extends BadRequestException {
|
|
65
|
+
/** Machine-readable error category. */
|
|
66
|
+
readonly code: FilterErrorCode;
|
|
67
|
+
/** Structured context about where the error occurred. */
|
|
68
|
+
readonly context: Readonly<Record<string, unknown>>;
|
|
69
|
+
constructor(code: FilterErrorCode, message: string, context?: Record<string, unknown>);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when the **schema** a caller declared cannot be translated — a
|
|
73
|
+
* `many-to-many` with target fields but no `table`, a `fields` entry naming a
|
|
74
|
+
* column that is not on the table, a nested `fields` map with no matching
|
|
75
|
+
* `relations` entry, or a table with no `id` and no explicit key.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately a **500**, not a {@link FilterException} 400: the request was
|
|
78
|
+
* well-formed and the whitelist accepted it — what is broken is the schema the
|
|
79
|
+
* plugin author wrote. Answering 400 would blame the client, hide the fault
|
|
80
|
+
* from alerting (4xx is a client error) and leave the schema bug in place. What
|
|
81
|
+
* the bare `Error` these replace got wrong is only that it was untyped and
|
|
82
|
+
* indistinguishable from a genuine crash: a named class with a `code` lets a
|
|
83
|
+
* transport adapter (MCP's JSON-RPC mapping, the copilot run loop) report
|
|
84
|
+
* "this endpoint's filter surface is misconfigured" instead of an opaque
|
|
85
|
+
* "internal error".
|
|
86
|
+
*/
|
|
87
|
+
export declare class FilterSchemaException extends InternalServerErrorException {
|
|
88
|
+
/** Machine-readable error category — always `FILTER_SCHEMA_INVALID`. */
|
|
89
|
+
readonly code = "FILTER_SCHEMA_INVALID";
|
|
90
|
+
constructor(message: string);
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=filter-exceptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-exceptions.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/filter-exceptions.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,4BAA4B,EAC/B,MAAM,gBAAgB,CAAC;AAExB,sDAAsD;AACtD,eAAO,MAAM,eAAe;IACxB,2DAA2D;;IAE3D,mDAAmD;;IAEnD,iDAAiD;;IAEjD,sDAAsD;;IAEtD;;;;;;OAMG;;IAEH,8DAA8D;;IAE9D,oEAAoE;;IAEpE,uEAAuE;;IAEvE,uDAAuD;;IAEvD,2DAA2D;;IAE3D,oDAAoD;;IAEpD,iEAAiE;;IAEjE;;;;OAIG;;IAEH,6EAA6E;;CAEvE,CAAC;AAEX,iDAAiD;AACjD,MAAM,MAAM,eAAe,GACvB,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,OAAO,eAAe,CAAC,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAgB,SAAQ,mBAAmB;IACpD,uCAAuC;IACvC,SAAgB,IAAI,EAAE,eAAe,CAAC;IACtC,yDAAyD;IACzD,SAAgB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;gBAGvD,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAkB5C;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,qBAAsB,SAAQ,4BAA4B;IACnE,wEAAwE;IACxE,SAAgB,IAAI,2BAA2B;gBAEnC,OAAO,EAAE,MAAM;CAQ9B"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FilterSchemaException = exports.FilterException = exports.FilterErrorCode = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
/** Machine-readable codes for filter parse errors. */
|
|
6
|
+
exports.FilterErrorCode = {
|
|
7
|
+
/** `filter` was not an object (e.g. a string or array). */
|
|
8
|
+
InvalidShape: 'FILTER_INVALID_SHAPE',
|
|
9
|
+
/** Parser reached a leaf with no path segments. */
|
|
10
|
+
EmptyPath: 'FILTER_EMPTY_PATH',
|
|
11
|
+
/** Dotted path longer than `schema.maxDepth`. */
|
|
12
|
+
DepthExceeded: 'FILTER_DEPTH_EXCEEDED',
|
|
13
|
+
/** Operator not in the global operator vocabulary. */
|
|
14
|
+
UnknownOperator: 'FILTER_UNKNOWN_OPERATOR',
|
|
15
|
+
/**
|
|
16
|
+
* Operator is a real one, but not one the field's declared type can be
|
|
17
|
+
* asked — e.g. `ilike` against a `date`, a `number`, a `boolean` or a
|
|
18
|
+
* `uuid`. Postgres has no `~~` for those column types, so without this the
|
|
19
|
+
* rule reached the driver and came back as an unhandled 500 instead of a
|
|
20
|
+
* per-field issue.
|
|
21
|
+
*/
|
|
22
|
+
OperatorNotAllowed: 'FILTER_OPERATOR_NOT_ALLOWED',
|
|
23
|
+
/** Final segment names a field not declared on the schema. */
|
|
24
|
+
UnknownField: 'FILTER_UNKNOWN_FIELD',
|
|
25
|
+
/** Mid-path segment names a relation not declared on the schema. */
|
|
26
|
+
UnknownRelation: 'FILTER_UNKNOWN_RELATION',
|
|
27
|
+
/** Value failed type coercion (uuid/number/boolean/date/enum/null). */
|
|
28
|
+
InvalidValue: 'FILTER_INVALID_VALUE',
|
|
29
|
+
/** `filter` was a string but did not parse as JSON. */
|
|
30
|
+
InvalidJson: 'FILTER_INVALID_JSON',
|
|
31
|
+
/** Tree node was neither a recognised group nor a rule. */
|
|
32
|
+
InvalidNode: 'FILTER_INVALID_NODE',
|
|
33
|
+
/** Tree exceeded `schema.maxNodes` (default 50). */
|
|
34
|
+
MaxNodesExceeded: 'FILTER_MAX_NODES_EXCEEDED',
|
|
35
|
+
/** Group nesting exceeded `schema.maxGroupDepth` (default 5). */
|
|
36
|
+
GroupDepthExceeded: 'FILTER_GROUP_DEPTH_EXCEEDED',
|
|
37
|
+
/**
|
|
38
|
+
* `in`/`nin` value list was empty. Rejected because an empty `in`
|
|
39
|
+
* matches no rows and an empty `nin` matches every row — a silent
|
|
40
|
+
* no-op/inverted filter rather than the user's intent.
|
|
41
|
+
*/
|
|
42
|
+
EmptyInList: 'FILTER_EMPTY_IN_LIST',
|
|
43
|
+
/** `in`/`nin` value list exceeded `schema.maxInListLength` (default 100). */
|
|
44
|
+
MaxInListExceeded: 'FILTER_MAX_IN_LIST_EXCEEDED'
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Typed 400 exception thrown by the filter parser.
|
|
48
|
+
*
|
|
49
|
+
* Still a `BadRequestException` under the hood so NestJS returns HTTP 400,
|
|
50
|
+
* but the response body carries a `code` (from {@link FilterErrorCode}) and
|
|
51
|
+
* a `context` object so clients can key off the category rather than
|
|
52
|
+
* string-matching the message.
|
|
53
|
+
*
|
|
54
|
+
* Example response body:
|
|
55
|
+
* ```json
|
|
56
|
+
* {
|
|
57
|
+
* "statusCode": 400,
|
|
58
|
+
* "error": "Bad Request",
|
|
59
|
+
* "code": "FILTER_UNKNOWN_FIELD",
|
|
60
|
+
* "message": "filter: unknown field \"secretField\"",
|
|
61
|
+
* "path": "secretField"
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
class FilterException extends common_1.BadRequestException {
|
|
66
|
+
/** Machine-readable error category. */
|
|
67
|
+
code;
|
|
68
|
+
/** Structured context about where the error occurred. */
|
|
69
|
+
context;
|
|
70
|
+
constructor(code, message, context = {}) {
|
|
71
|
+
// `context` is spread FIRST so the four reserved keys always win. The
|
|
72
|
+
// whole contract of this class is that a client can branch on `code`
|
|
73
|
+
// and on the 400 status; a context key named `code` / `statusCode` /
|
|
74
|
+
// `error` / `message` must not be able to rewrite them — a caller adding
|
|
75
|
+
// a plausible context field (`message: 'why'`) would otherwise silently
|
|
76
|
+
// break every consumer of the envelope.
|
|
77
|
+
super({
|
|
78
|
+
...context,
|
|
79
|
+
statusCode: 400,
|
|
80
|
+
error: 'Bad Request',
|
|
81
|
+
code,
|
|
82
|
+
message: `filter: ${message}`
|
|
83
|
+
});
|
|
84
|
+
this.code = code;
|
|
85
|
+
this.context = context;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.FilterException = FilterException;
|
|
89
|
+
/**
|
|
90
|
+
* Thrown when the **schema** a caller declared cannot be translated — a
|
|
91
|
+
* `many-to-many` with target fields but no `table`, a `fields` entry naming a
|
|
92
|
+
* column that is not on the table, a nested `fields` map with no matching
|
|
93
|
+
* `relations` entry, or a table with no `id` and no explicit key.
|
|
94
|
+
*
|
|
95
|
+
* Deliberately a **500**, not a {@link FilterException} 400: the request was
|
|
96
|
+
* well-formed and the whitelist accepted it — what is broken is the schema the
|
|
97
|
+
* plugin author wrote. Answering 400 would blame the client, hide the fault
|
|
98
|
+
* from alerting (4xx is a client error) and leave the schema bug in place. What
|
|
99
|
+
* the bare `Error` these replace got wrong is only that it was untyped and
|
|
100
|
+
* indistinguishable from a genuine crash: a named class with a `code` lets a
|
|
101
|
+
* transport adapter (MCP's JSON-RPC mapping, the copilot run loop) report
|
|
102
|
+
* "this endpoint's filter surface is misconfigured" instead of an opaque
|
|
103
|
+
* "internal error".
|
|
104
|
+
*/
|
|
105
|
+
class FilterSchemaException extends common_1.InternalServerErrorException {
|
|
106
|
+
/** Machine-readable error category — always `FILTER_SCHEMA_INVALID`. */
|
|
107
|
+
code = 'FILTER_SCHEMA_INVALID';
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super({
|
|
110
|
+
statusCode: 500,
|
|
111
|
+
error: 'Internal Server Error',
|
|
112
|
+
code: 'FILTER_SCHEMA_INVALID',
|
|
113
|
+
message: `filter schema: ${message}`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.FilterSchemaException = FilterSchemaException;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FilterOperator } from './types';
|
|
2
|
+
import type { ParsedFilter } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a leaf asks for the ABSENCE of a match rather than the presence
|
|
5
|
+
* of one. `null` is operator-and-value dependent: `value: true` is
|
|
6
|
+
* "is empty" (negating), `value: false` is "is not empty" (positive).
|
|
7
|
+
*/
|
|
8
|
+
export declare function isNegatingLeaf(op: FilterOperator, value: unknown): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The positive twin of a negating leaf — the predicate whose *absence*
|
|
11
|
+
* the original asserts. `ne`→`eq`, `nin`→`in`, `nilike`→`ilike`, and
|
|
12
|
+
* "is empty"→"is not empty".
|
|
13
|
+
*
|
|
14
|
+
* Used to rewrite a negating rule on a RELATION path from
|
|
15
|
+
* `EXISTS(… NOT p …)` into `NOT EXISTS(… p …)`. The two are not the same
|
|
16
|
+
* statement once a relation can hold more than one row:
|
|
17
|
+
*
|
|
18
|
+
* - `tags.name is-none-of ['x']` as `EXISTS(tag WHERE name <> 'x')` means
|
|
19
|
+
* "has SOME tag that isn't x" — an entry tagged `[x, y]` matches, which
|
|
20
|
+
* is the opposite of what the filter says. As
|
|
21
|
+
* `NOT EXISTS(tag WHERE name = 'x')` it means "has NO tag named x". ✔
|
|
22
|
+
* - `author.id is-empty` as `EXISTS(author WHERE author.id IS NULL)` can
|
|
23
|
+
* never be true (`id` is a NOT NULL primary key) — a dead filter. As
|
|
24
|
+
* `NOT EXISTS(author WHERE author.id IS NOT NULL)` it means "has no
|
|
25
|
+
* author". ✔
|
|
26
|
+
*
|
|
27
|
+
* For a to-ONE relation the rewrite also reads correctly and, as a bonus,
|
|
28
|
+
* stops silently dropping rows whose FK is null: "author is not Ada" now
|
|
29
|
+
* includes entries with no author at all, matching the same rule's
|
|
30
|
+
* behaviour on a plain column (see `scalar()`'s null handling).
|
|
31
|
+
*/
|
|
32
|
+
export declare function positiveLeaf(leaf: ParsedFilter): ParsedFilter;
|
|
33
|
+
//# sourceMappingURL=negation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negation.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/negation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAW1E;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAa7D"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isNegatingLeaf = isNegatingLeaf;
|
|
4
|
+
exports.positiveLeaf = positiveLeaf;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/**
|
|
7
|
+
* Whether a leaf asks for the ABSENCE of a match rather than the presence
|
|
8
|
+
* of one. `null` is operator-and-value dependent: `value: true` is
|
|
9
|
+
* "is empty" (negating), `value: false` is "is not empty" (positive).
|
|
10
|
+
*/
|
|
11
|
+
function isNegatingLeaf(op, value) {
|
|
12
|
+
switch (op) {
|
|
13
|
+
case types_1.FilterOperator.Ne:
|
|
14
|
+
case types_1.FilterOperator.Nin:
|
|
15
|
+
case types_1.FilterOperator.Nilike:
|
|
16
|
+
return true;
|
|
17
|
+
case types_1.FilterOperator.Null:
|
|
18
|
+
return value === true;
|
|
19
|
+
default:
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The positive twin of a negating leaf — the predicate whose *absence*
|
|
25
|
+
* the original asserts. `ne`→`eq`, `nin`→`in`, `nilike`→`ilike`, and
|
|
26
|
+
* "is empty"→"is not empty".
|
|
27
|
+
*
|
|
28
|
+
* Used to rewrite a negating rule on a RELATION path from
|
|
29
|
+
* `EXISTS(… NOT p …)` into `NOT EXISTS(… p …)`. The two are not the same
|
|
30
|
+
* statement once a relation can hold more than one row:
|
|
31
|
+
*
|
|
32
|
+
* - `tags.name is-none-of ['x']` as `EXISTS(tag WHERE name <> 'x')` means
|
|
33
|
+
* "has SOME tag that isn't x" — an entry tagged `[x, y]` matches, which
|
|
34
|
+
* is the opposite of what the filter says. As
|
|
35
|
+
* `NOT EXISTS(tag WHERE name = 'x')` it means "has NO tag named x". ✔
|
|
36
|
+
* - `author.id is-empty` as `EXISTS(author WHERE author.id IS NULL)` can
|
|
37
|
+
* never be true (`id` is a NOT NULL primary key) — a dead filter. As
|
|
38
|
+
* `NOT EXISTS(author WHERE author.id IS NOT NULL)` it means "has no
|
|
39
|
+
* author". ✔
|
|
40
|
+
*
|
|
41
|
+
* For a to-ONE relation the rewrite also reads correctly and, as a bonus,
|
|
42
|
+
* stops silently dropping rows whose FK is null: "author is not Ada" now
|
|
43
|
+
* includes entries with no author at all, matching the same rule's
|
|
44
|
+
* behaviour on a plain column (see `scalar()`'s null handling).
|
|
45
|
+
*/
|
|
46
|
+
function positiveLeaf(leaf) {
|
|
47
|
+
switch (leaf.op) {
|
|
48
|
+
case types_1.FilterOperator.Ne:
|
|
49
|
+
return { ...leaf, op: types_1.FilterOperator.Eq };
|
|
50
|
+
case types_1.FilterOperator.Nin:
|
|
51
|
+
return { ...leaf, op: types_1.FilterOperator.In };
|
|
52
|
+
case types_1.FilterOperator.Nilike:
|
|
53
|
+
return { ...leaf, op: types_1.FilterOperator.Ilike };
|
|
54
|
+
case types_1.FilterOperator.Null:
|
|
55
|
+
return { ...leaf, op: types_1.FilterOperator.Null, value: false };
|
|
56
|
+
default:
|
|
57
|
+
return leaf;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FilterOperator, ScalarFieldType } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Operators offered per declared scalar type. See the note above for why this
|
|
4
|
+
* is wider than the admin's picker table.
|
|
5
|
+
*
|
|
6
|
+
* `enum` is text-backed (a `select` field is a `text` column), so the pattern
|
|
7
|
+
* operators are legal SQL on it — and unreachable in practice anyway, since
|
|
8
|
+
* value coercion already requires an exact `enumValues` member. Listing them
|
|
9
|
+
* keeps this table a statement about the **column**, which is what it is for.
|
|
10
|
+
*/
|
|
11
|
+
export declare const OPERATORS_BY_TYPE: Record<ScalarFieldType, readonly FilterOperator[]>;
|
|
12
|
+
/**
|
|
13
|
+
* The operators `type` accepts, or `undefined` when the type is not one this
|
|
14
|
+
* table knows.
|
|
15
|
+
*
|
|
16
|
+
* `Object.hasOwn`, not a bare lookup: `OPERATORS_BY_TYPE` is a plain object
|
|
17
|
+
* literal and a `FilterSchema` can be built at runtime (the content plugin
|
|
18
|
+
* derives one per content type), so a field declaring `type: 'constructor'`
|
|
19
|
+
* would otherwise read back a truthy non-array. An unknown type is reported by
|
|
20
|
+
* `scalarOf`'s `FilterSchemaException` — a schema bug, a 500 — so this returns
|
|
21
|
+
* `undefined` and lets the operator check stand aside rather than blame the
|
|
22
|
+
* client for it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function operatorsFor(type: ScalarFieldType): readonly FilterOperator[] | undefined;
|
|
25
|
+
//# sourceMappingURL=operator-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operator-support.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/operator-support.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAsD1D;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAClC,eAAe,EACf,SAAS,cAAc,EAAE,CAQ5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CACxB,IAAI,EAAE,eAAe,GACtB,SAAS,cAAc,EAAE,GAAG,SAAS,CAIvC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OPERATORS_BY_TYPE = void 0;
|
|
4
|
+
exports.operatorsFor = operatorsFor;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/**
|
|
7
|
+
* Which operators each declared scalar type can actually be asked.
|
|
8
|
+
*
|
|
9
|
+
* The third axis of a filter leaf. `resolveLeaf` already validated the **field**
|
|
10
|
+
* (against the schema whitelist) and the **value** (against the field's declared
|
|
11
|
+
* type); nothing validated that the **operator** is one the underlying column
|
|
12
|
+
* can answer. So `?filter={"field":"embargoUntil","op":"ilike","value":"%2020%"}`
|
|
13
|
+
* passed both checks and reached Postgres as
|
|
14
|
+
* `"embargo_until" ilike $3`, which raises
|
|
15
|
+
* `operator does not exist: timestamp with time zone ~~* unknown` — a
|
|
16
|
+
* **user-triggerable 500 from a shareable link** on every filterable endpoint,
|
|
17
|
+
* reported as an unhandled driver error rather than as a per-field issue the
|
|
18
|
+
* client can render.
|
|
19
|
+
*
|
|
20
|
+
* The table is drawn from what the **column type can answer**, not from what a
|
|
21
|
+
* UI chooses to offer. The admin's `OPS_FOR_TYPE` is deliberately narrower —
|
|
22
|
+
* it drops `eq` on dates (its editor is minute-precision against a millisecond
|
|
23
|
+
* `timestamptz`) and everything but `eq` on booleans — but those are editor
|
|
24
|
+
* ergonomics, not SQL legality, and this is the boundary an API token, a
|
|
25
|
+
* hand-written URL and the GraphQL adapter all cross. Rejecting a legal
|
|
26
|
+
* `publishedAt eq <instant>` here would be a regression, not a fix.
|
|
27
|
+
*
|
|
28
|
+
* What that leaves is the pattern family: `like` / `ilike` / `nilike` are the
|
|
29
|
+
* `~~` operator class, which Postgres defines for text only. Every non-textual
|
|
30
|
+
* column type therefore rejects them, and everything else stays as permissive
|
|
31
|
+
* as the database is. `null` (IS NULL) and `in`/`nin` are legal on every type.
|
|
32
|
+
*/
|
|
33
|
+
const PATTERN_OPERATORS = [
|
|
34
|
+
types_1.FilterOperator.Like,
|
|
35
|
+
types_1.FilterOperator.Ilike,
|
|
36
|
+
types_1.FilterOperator.Nilike
|
|
37
|
+
];
|
|
38
|
+
/** Comparison + membership + null — legal on every scalar type. */
|
|
39
|
+
const COMPARABLE_OPERATORS = [
|
|
40
|
+
types_1.FilterOperator.Eq,
|
|
41
|
+
types_1.FilterOperator.Ne,
|
|
42
|
+
types_1.FilterOperator.Gt,
|
|
43
|
+
types_1.FilterOperator.Gte,
|
|
44
|
+
types_1.FilterOperator.Lt,
|
|
45
|
+
types_1.FilterOperator.Lte,
|
|
46
|
+
types_1.FilterOperator.In,
|
|
47
|
+
types_1.FilterOperator.Nin,
|
|
48
|
+
types_1.FilterOperator.Null
|
|
49
|
+
];
|
|
50
|
+
/** {@link COMPARABLE_OPERATORS} plus the text-only `~~` family. */
|
|
51
|
+
const TEXT_OPERATORS = [
|
|
52
|
+
...COMPARABLE_OPERATORS,
|
|
53
|
+
...PATTERN_OPERATORS
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Operators offered per declared scalar type. See the note above for why this
|
|
57
|
+
* is wider than the admin's picker table.
|
|
58
|
+
*
|
|
59
|
+
* `enum` is text-backed (a `select` field is a `text` column), so the pattern
|
|
60
|
+
* operators are legal SQL on it — and unreachable in practice anyway, since
|
|
61
|
+
* value coercion already requires an exact `enumValues` member. Listing them
|
|
62
|
+
* keeps this table a statement about the **column**, which is what it is for.
|
|
63
|
+
*/
|
|
64
|
+
exports.OPERATORS_BY_TYPE = {
|
|
65
|
+
[types_1.ScalarFieldType.String]: TEXT_OPERATORS,
|
|
66
|
+
[types_1.ScalarFieldType.Enum]: TEXT_OPERATORS,
|
|
67
|
+
[types_1.ScalarFieldType.Number]: COMPARABLE_OPERATORS,
|
|
68
|
+
[types_1.ScalarFieldType.Boolean]: COMPARABLE_OPERATORS,
|
|
69
|
+
[types_1.ScalarFieldType.Uuid]: COMPARABLE_OPERATORS,
|
|
70
|
+
[types_1.ScalarFieldType.Date]: COMPARABLE_OPERATORS
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The operators `type` accepts, or `undefined` when the type is not one this
|
|
74
|
+
* table knows.
|
|
75
|
+
*
|
|
76
|
+
* `Object.hasOwn`, not a bare lookup: `OPERATORS_BY_TYPE` is a plain object
|
|
77
|
+
* literal and a `FilterSchema` can be built at runtime (the content plugin
|
|
78
|
+
* derives one per content type), so a field declaring `type: 'constructor'`
|
|
79
|
+
* would otherwise read back a truthy non-array. An unknown type is reported by
|
|
80
|
+
* `scalarOf`'s `FilterSchemaException` — a schema bug, a 500 — so this returns
|
|
81
|
+
* `undefined` and lets the operator check stand aside rather than blame the
|
|
82
|
+
* client for it.
|
|
83
|
+
*/
|
|
84
|
+
function operatorsFor(type) {
|
|
85
|
+
return Object.hasOwn(exports.OPERATORS_BY_TYPE, type)
|
|
86
|
+
? exports.OPERATORS_BY_TYPE[type]
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Look a **user-supplied** name up in a schema map, ignoring anything the map
|
|
3
|
+
* merely inherits.
|
|
4
|
+
*
|
|
5
|
+
* Every whitelist in this package — `FilterSchema.fields`, `.relations`, a
|
|
6
|
+
* `RelationSchema`'s nested maps, drizzle's own column map — is a plain object
|
|
7
|
+
* literal, so a bare `map[name]` also resolves `constructor`, `toString`,
|
|
8
|
+
* `valueOf`, `hasOwnProperty` and the rest of `Object.prototype` to truthy
|
|
9
|
+
* values. A `!map[name]` guard therefore accepts a name nobody declared, and
|
|
10
|
+
* the schema stops being the security boundary it is documented to be.
|
|
11
|
+
*
|
|
12
|
+
* Downstream neither outcome is a harmless no-match:
|
|
13
|
+
*
|
|
14
|
+
* - a scalar path (`?filter={"field":"constructor",…}`) reaches `columnOf`,
|
|
15
|
+
* which resolves the same inherited member off the column map and hands a
|
|
16
|
+
* `Function` to drizzle as a `Column`. The emitted fragment is `$1 = ` —
|
|
17
|
+
* a Postgres syntax error, i.e. a **user-triggerable 500** on every
|
|
18
|
+
* filterable endpoint.
|
|
19
|
+
* - a relation-shaped path (`?filter={"field":"toString.constructor",…}`)
|
|
20
|
+
* falls out of `relationExists`'s `switch (rel.kind)` as `undefined`, so the
|
|
21
|
+
* predicate is **silently dropped** and the endpoint answers 200 unfiltered.
|
|
22
|
+
*
|
|
23
|
+
* Returning `undefined` here lets each call site raise its own
|
|
24
|
+
* `FILTER_UNKNOWN_FIELD` / `FILTER_UNKNOWN_RELATION`, which is what a caller
|
|
25
|
+
* asking for an undeclared name should always have got.
|
|
26
|
+
*/
|
|
27
|
+
export declare function own<T>(map: Record<string, T> | undefined, name: string): T | undefined;
|
|
28
|
+
//# sourceMappingURL=own-property.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"own-property.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/own-property.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,GAAG,CAAC,CAAC,EACjB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,SAAS,EAClC,IAAI,EAAE,MAAM,GACb,CAAC,GAAG,SAAS,CAGf"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.own = own;
|
|
4
|
+
/**
|
|
5
|
+
* Look a **user-supplied** name up in a schema map, ignoring anything the map
|
|
6
|
+
* merely inherits.
|
|
7
|
+
*
|
|
8
|
+
* Every whitelist in this package — `FilterSchema.fields`, `.relations`, a
|
|
9
|
+
* `RelationSchema`'s nested maps, drizzle's own column map — is a plain object
|
|
10
|
+
* literal, so a bare `map[name]` also resolves `constructor`, `toString`,
|
|
11
|
+
* `valueOf`, `hasOwnProperty` and the rest of `Object.prototype` to truthy
|
|
12
|
+
* values. A `!map[name]` guard therefore accepts a name nobody declared, and
|
|
13
|
+
* the schema stops being the security boundary it is documented to be.
|
|
14
|
+
*
|
|
15
|
+
* Downstream neither outcome is a harmless no-match:
|
|
16
|
+
*
|
|
17
|
+
* - a scalar path (`?filter={"field":"constructor",…}`) reaches `columnOf`,
|
|
18
|
+
* which resolves the same inherited member off the column map and hands a
|
|
19
|
+
* `Function` to drizzle as a `Column`. The emitted fragment is `$1 = ` —
|
|
20
|
+
* a Postgres syntax error, i.e. a **user-triggerable 500** on every
|
|
21
|
+
* filterable endpoint.
|
|
22
|
+
* - a relation-shaped path (`?filter={"field":"toString.constructor",…}`)
|
|
23
|
+
* falls out of `relationExists`'s `switch (rel.kind)` as `undefined`, so the
|
|
24
|
+
* predicate is **silently dropped** and the endpoint answers 200 unfiltered.
|
|
25
|
+
*
|
|
26
|
+
* Returning `undefined` here lets each call site raise its own
|
|
27
|
+
* `FILTER_UNKNOWN_FIELD` / `FILTER_UNKNOWN_RELATION`, which is what a caller
|
|
28
|
+
* asking for an undeclared name should always have got.
|
|
29
|
+
*/
|
|
30
|
+
function own(map, name) {
|
|
31
|
+
if (!map)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Object.hasOwn(map, name) ? map[name] : undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FilterSchema, ParsedNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a `filter` payload into a tree the translator can walk. Two
|
|
4
|
+
* input shapes are accepted:
|
|
5
|
+
*
|
|
6
|
+
* 1. `string` — a JSON-encoded tree, so a controller can forward
|
|
7
|
+
* `?filter=<json>` without pre-parsing. Throws `InvalidJson` on
|
|
8
|
+
* malformed input.
|
|
9
|
+
* 2. Object — already-parsed tree (`{ and: [...] }` / `{ or: [...] }` /
|
|
10
|
+
* single rule `{ field, op, value }`). Recursively validated; each
|
|
11
|
+
* leaf flows through the same schema/op/coercion check.
|
|
12
|
+
*
|
|
13
|
+
* Returns `null` when the input is missing, empty, or `{}` so callers
|
|
14
|
+
* can skip the WHERE clause without an empty-array dance.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseFilterTree(rawFilter: unknown, schema: FilterSchema): ParsedNode | null;
|
|
17
|
+
//# sourceMappingURL=parse-filter-tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-filter-tree.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/parse-filter-tree.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAgB,UAAU,EAAE,MAAM,SAAS,CAAC;AAOtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC3B,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,YAAY,GACrB,UAAU,GAAG,IAAI,CAoCnB"}
|