@orthacms/utils-server 0.4.2 → 0.5.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/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export { parseFilterTree } from './lib/filters/parse-filter-tree';
2
+ export { FILTER_MAX_LENGTH } from './lib/filters/budgets';
2
3
  export { applyFilterTree, type ApplyFilterTreeOptions, type FilterExtensionResolver } from './lib/filters/tree-to-drizzle';
3
4
  export type { DbLike, TableLike } from './lib/filters/table-helpers';
4
- export { FilterOperator, ScalarFieldType, RelationKind } from './lib/filters/types';
5
+ export { FilterOperator, ScalarFieldType, RelationKind, WithinLastUnit } from './lib/filters/types';
5
6
  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 type { WithinLastValue, ScalarFieldSchema, FieldSchema, RelationSchema, RelationScope, FilterSchema, ParsedRule, ParsedGroup, ParsedNode } from './lib/filters/types';
7
8
  export { FilterException, FilterErrorCode, FilterSchemaException } from './lib/filters/filter-exceptions';
8
9
  export { clampInt } from './lib/clamp-int';
9
10
  export { isForeignKeyViolation, isUniqueViolation, violatedConstraint } from './lib/pg-errors';
11
+ export { readEnv, requireEnv, readPositiveInt, readOptionalPositiveInt, readList, readOptionalList, readFlag, readTrustProxy, readNodeEnv, isProduction, when, defined, NODE_ENVS, type NodeEnv } from './lib/env';
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,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,EACZ,cAAc,EACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,iBAAiB,EACjB,YAAY,EACf,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACR,eAAe,EACf,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;AACzB,OAAO,EACH,OAAO,EACP,UAAU,EACV,eAAe,EACf,uBAAuB,EACvB,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,WAAW,EACX,YAAY,EACZ,IAAI,EACJ,OAAO,EACP,SAAS,EACT,KAAK,OAAO,EACf,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -1,14 +1,17 @@
1
1
  "use strict";
2
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;
3
+ exports.NODE_ENVS = exports.defined = exports.when = exports.isProduction = exports.readNodeEnv = exports.readTrustProxy = exports.readFlag = exports.readOptionalList = exports.readList = exports.readOptionalPositiveInt = exports.readPositiveInt = exports.requireEnv = exports.readEnv = exports.violatedConstraint = exports.isUniqueViolation = exports.isForeignKeyViolation = exports.clampInt = exports.FilterSchemaException = exports.FilterErrorCode = exports.FilterException = exports.operatorsFor = exports.OPERATORS_BY_TYPE = exports.WithinLastUnit = exports.RelationKind = exports.ScalarFieldType = exports.FilterOperator = exports.applyFilterTree = exports.FILTER_MAX_LENGTH = exports.parseFilterTree = void 0;
4
4
  var parse_filter_tree_1 = require("./lib/filters/parse-filter-tree");
5
5
  Object.defineProperty(exports, "parseFilterTree", { enumerable: true, get: function () { return parse_filter_tree_1.parseFilterTree; } });
6
+ var budgets_1 = require("./lib/filters/budgets");
7
+ Object.defineProperty(exports, "FILTER_MAX_LENGTH", { enumerable: true, get: function () { return budgets_1.FILTER_MAX_LENGTH; } });
6
8
  var tree_to_drizzle_1 = require("./lib/filters/tree-to-drizzle");
7
9
  Object.defineProperty(exports, "applyFilterTree", { enumerable: true, get: function () { return tree_to_drizzle_1.applyFilterTree; } });
8
10
  var types_1 = require("./lib/filters/types");
9
11
  Object.defineProperty(exports, "FilterOperator", { enumerable: true, get: function () { return types_1.FilterOperator; } });
10
12
  Object.defineProperty(exports, "ScalarFieldType", { enumerable: true, get: function () { return types_1.ScalarFieldType; } });
11
13
  Object.defineProperty(exports, "RelationKind", { enumerable: true, get: function () { return types_1.RelationKind; } });
14
+ Object.defineProperty(exports, "WithinLastUnit", { enumerable: true, get: function () { return types_1.WithinLastUnit; } });
12
15
  var operator_support_1 = require("./lib/filters/operator-support");
13
16
  Object.defineProperty(exports, "OPERATORS_BY_TYPE", { enumerable: true, get: function () { return operator_support_1.OPERATORS_BY_TYPE; } });
14
17
  Object.defineProperty(exports, "operatorsFor", { enumerable: true, get: function () { return operator_support_1.operatorsFor; } });
@@ -22,3 +25,17 @@ var pg_errors_1 = require("./lib/pg-errors");
22
25
  Object.defineProperty(exports, "isForeignKeyViolation", { enumerable: true, get: function () { return pg_errors_1.isForeignKeyViolation; } });
23
26
  Object.defineProperty(exports, "isUniqueViolation", { enumerable: true, get: function () { return pg_errors_1.isUniqueViolation; } });
24
27
  Object.defineProperty(exports, "violatedConstraint", { enumerable: true, get: function () { return pg_errors_1.violatedConstraint; } });
28
+ var env_1 = require("./lib/env");
29
+ Object.defineProperty(exports, "readEnv", { enumerable: true, get: function () { return env_1.readEnv; } });
30
+ Object.defineProperty(exports, "requireEnv", { enumerable: true, get: function () { return env_1.requireEnv; } });
31
+ Object.defineProperty(exports, "readPositiveInt", { enumerable: true, get: function () { return env_1.readPositiveInt; } });
32
+ Object.defineProperty(exports, "readOptionalPositiveInt", { enumerable: true, get: function () { return env_1.readOptionalPositiveInt; } });
33
+ Object.defineProperty(exports, "readList", { enumerable: true, get: function () { return env_1.readList; } });
34
+ Object.defineProperty(exports, "readOptionalList", { enumerable: true, get: function () { return env_1.readOptionalList; } });
35
+ Object.defineProperty(exports, "readFlag", { enumerable: true, get: function () { return env_1.readFlag; } });
36
+ Object.defineProperty(exports, "readTrustProxy", { enumerable: true, get: function () { return env_1.readTrustProxy; } });
37
+ Object.defineProperty(exports, "readNodeEnv", { enumerable: true, get: function () { return env_1.readNodeEnv; } });
38
+ Object.defineProperty(exports, "isProduction", { enumerable: true, get: function () { return env_1.isProduction; } });
39
+ Object.defineProperty(exports, "when", { enumerable: true, get: function () { return env_1.when; } });
40
+ Object.defineProperty(exports, "defined", { enumerable: true, get: function () { return env_1.defined; } });
41
+ Object.defineProperty(exports, "NODE_ENVS", { enumerable: true, get: function () { return env_1.NODE_ENVS; } });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Reading `process.env` into typed configuration.
3
+ *
4
+ * A host's `ortha.config.ts` is the one file allowed to touch the environment,
5
+ * and every deployment writes the same handful of readers to do it: a required
6
+ * string, a bounded number, a comma list, `TRUST_PROXY`, `NODE_ENV`. They were
7
+ * duplicated between this repo's host and the scaffolder's template, where the
8
+ * copies had already drifted — the template's number reader had no
9
+ * "optional, no default" form, so a generated app could not express a ceiling
10
+ * that means "leave the plugin's own default alone".
11
+ *
12
+ * Every reader here **throws rather than guesses**. That is the whole point of
13
+ * the module: the failure mode of environment parsing is silence, and a config
14
+ * value that is quietly wrong is discovered a session TTL later, or by reading
15
+ * a `Set-Cookie` header, rather than at boot with the variable named.
16
+ *
17
+ * The last two exports — {@link when} and {@link defined} — read nothing. They
18
+ * are here because they are what a host does with the readers' `undefined`:
19
+ * plugins merge their settings as `{ ...DEFAULTS, ...config }`, so an unset
20
+ * value must become an *absent key* rather than an explicit `undefined`, which
21
+ * would erase the default it was meant to leave alone. Every host writing that
22
+ * as `...(x ? { key } : {})` at each call site is the duplication this module
23
+ * exists to end.
24
+ */
25
+ /**
26
+ * A trimmed environment value, `undefined` when unset **or empty**.
27
+ *
28
+ * The one reader every other reader here is built on, so "empty means not
29
+ * configured" is decided once. That equivalence matters: `.env.example` ships
30
+ * keys with no value, and `ANTHROPIC_API_KEY=` has to leave a deployment with
31
+ * *no* Claude backend rather than one registered with an empty key — a backend
32
+ * that is in the picker and fails on the first message.
33
+ */
34
+ export declare function readEnv(name: string): string | undefined;
35
+ /**
36
+ * Reads a value the app cannot run without, failing at load rather than
37
+ * several seconds into boot.
38
+ *
39
+ * Left to default to `''`, a missing `DATABASE_URL` reaches `pg` as "use the
40
+ * libpq defaults", and the first thing that touches the database fails with
41
+ * whatever the local libpq environment happens to produce (measured: `SASL:
42
+ * SCRAM-SERVER-FIRST-MESSAGE: client password must be a string`). The server
43
+ * does fail closed, which is the important half, but nothing in that message
44
+ * names the variable that was never set.
45
+ *
46
+ * @param name The environment variable to read.
47
+ * @param hint Appended to the error, for a deployment-specific instruction —
48
+ * which file to copy, which secret store to look in. Omitted, the message
49
+ * says only that there is no usable default.
50
+ */
51
+ export declare function requireEnv(name: string, hint?: string): string;
52
+ /**
53
+ * Reads a numeric setting: the default when unset or empty, the value when it
54
+ * is a plain positive decimal integer, and an error otherwise.
55
+ *
56
+ * This replaces `Number(process.env[x]) || default`, which was wrong in three
57
+ * directions at once and silent in all of them. `0` is falsy, so it became the
58
+ * default — `LOGIN_RATE_LIMIT=0` ("block every login") quietly meant 10. A
59
+ * negative is truthy, so it was accepted — `SESSION_TTL_SECONDS=-1` issued
60
+ * every session already expired, login answering `201` and the very next
61
+ * request `401`. And exponent notation parsed, so `GRAPHQL_MAX_DEPTH=1e9`
62
+ * removed the cost budget that ADR-0008 calls GraphQL's replacement for REST's
63
+ * structural bound. Refusing to boot names the variable; the alternative was a
64
+ * deployment that looked configured and was not.
65
+ *
66
+ * Empty is deliberately *not* an error: `.env.example` ships several keys with
67
+ * no value, and a fresh clone must boot from it unchanged.
68
+ */
69
+ export declare function readPositiveInt(name: string, fallback: number): number;
70
+ /**
71
+ * As {@link readPositiveInt}, but `undefined` when unset — no default to fall
72
+ * back to.
73
+ *
74
+ * The form a *ceiling* needs. A plugin that ships its own default cannot be
75
+ * handed `{ maxSteps: undefined }`, because spreading that overwrites the
76
+ * default with nothing; the caller conditionally spreads on this instead.
77
+ */
78
+ export declare function readOptionalPositiveInt(name: string): number | undefined;
79
+ /**
80
+ * A comma-separated list setting, trimmed and emptied of blanks.
81
+ *
82
+ * An explicitly empty value yields an empty list rather than the fallback,
83
+ * because "allow no origins" is a setting somebody means.
84
+ */
85
+ export declare function readList(name: string, fallback: string): string[];
86
+ /**
87
+ * Express's `trust proxy` setting, in the three shapes it accepts.
88
+ *
89
+ * Checked in this order: a hop count (`'1'` — the recommended form, and the
90
+ * only one a client cannot forge past), a boolean (`'true'` trusts the entire
91
+ * `X-Forwarded-For` chain, `'false'` trusts none), or any other non-empty
92
+ * string, passed to Express verbatim as a subnet/preset list (`'loopback'`,
93
+ * `'10.0.0.0/8'`). Unset yields `undefined`, leaving Express's default of
94
+ * ignoring forwarded headers entirely.
95
+ *
96
+ * Returns the union structurally rather than importing `TrustProxySetting`
97
+ * from `@orthacms/bootstrap-server`: this is a leaf helper package, and the
98
+ * host importing it must not become a dependency of it. The host's own
99
+ * `trustProxy?: TrustProxySetting` field is what checks the two agree.
100
+ */
101
+ export declare function readTrustProxy(name?: string): boolean | number | string | undefined;
102
+ /** The deployment modes an Ortha app recognises. */
103
+ export declare const NODE_ENVS: readonly ["development", "test", "production"];
104
+ /** One of {@link NODE_ENVS}. */
105
+ export type NodeEnv = (typeof NODE_ENVS)[number];
106
+ /**
107
+ * Reads `NODE_ENV`, rejecting a value that is neither recognised nor empty.
108
+ *
109
+ * `NODE_ENV !== 'production'` is the switch behind **two** protections at once
110
+ * — whether the API reference and GraphiQL are published, and whether the
111
+ * session cookie carries `Secure` — so any value that is not exactly
112
+ * `production` turns both off. Unset is a legitimate, and the common, local
113
+ * state; a *typo* is not, and it is indistinguishable from correct
114
+ * configuration until you read a `Set-Cookie` header. Measured: `produciton`
115
+ * serves `/reference/json` to an unauthenticated caller and drops `Secure`
116
+ * from the session cookie, exactly as if nothing had been set (ORT-137).
117
+ *
118
+ * Rejecting the typo costs a deployment that spells it right nothing, and
119
+ * turns a silent downgrade into a refusal to start.
120
+ */
121
+ export declare function readNodeEnv(): NodeEnv | undefined;
122
+ /**
123
+ * True only in a deployment that said `production`, with the spelling checked
124
+ * by {@link readNodeEnv}.
125
+ */
126
+ export declare function isProduction(): boolean;
127
+ /**
128
+ * As {@link readList}, but `undefined` when the variable is unset, so the
129
+ * consumer's own default list survives rather than being replaced by an empty
130
+ * one.
131
+ *
132
+ * The list counterpart to {@link readOptionalPositiveInt}, and needed for the
133
+ * same reason: a setting whose owner ships a default cannot be handed an empty
134
+ * array meaning "the deployment said nothing".
135
+ */
136
+ export declare function readOptionalList(name: string): string[] | undefined;
137
+ /**
138
+ * A boolean setting: what the deployment said, or `fallback` when it said
139
+ * nothing.
140
+ *
141
+ * Anything other than `true` reads as false, so a typo turns a switch **off**
142
+ * rather than on. That direction is deliberate: the switches this reads are
143
+ * kill switches for surfaces that send content to a third party or open a door
144
+ * for an external agent, and a misspelling should not open one.
145
+ */
146
+ export declare function readFlag(name: string, fallback: boolean): boolean;
147
+ /**
148
+ * The value when the setting was configured, `undefined` when it was not.
149
+ *
150
+ * The counterpart to {@link defined}: together they replace
151
+ * `...(x ? { key: … } : {})` at a config site. `build` is a thunk so its body —
152
+ * often several further reads — runs only when it applies, which is what lets a
153
+ * whole provider block sit behind one credential check.
154
+ */
155
+ export declare function when<T>(configured: unknown, build: () => T): T | undefined;
156
+ /**
157
+ * The same object with every `undefined`-valued key removed.
158
+ *
159
+ * A plugin merges its own defaults as `{ ...DEFAULTS, ...config }`, so an
160
+ * explicit `{ maxSteps: undefined }` does not leave `DEFAULT_RUN_LIMITS.maxSteps`
161
+ * in place — it erases it. Dropping the key is what "the deployment did not set
162
+ * this" has to mean, and saying it once at the end of a builder is why a config
163
+ * file no longer needs a conditional per optional setting.
164
+ */
165
+ export declare function defined<T extends object>(value: T): T;
166
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/lib/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAS9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcxE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAKjE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC1B,IAAI,SAAgB,GACrB,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAavC;AAED,oDAAoD;AACpD,eAAO,MAAM,SAAS,gDAAiD,CAAC;AAExE,gCAAgC;AAChC,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,IAAI,OAAO,GAAG,SAAS,CAejD;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAEnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAGjE;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAE1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAQrD"}
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ /**
3
+ * Reading `process.env` into typed configuration.
4
+ *
5
+ * A host's `ortha.config.ts` is the one file allowed to touch the environment,
6
+ * and every deployment writes the same handful of readers to do it: a required
7
+ * string, a bounded number, a comma list, `TRUST_PROXY`, `NODE_ENV`. They were
8
+ * duplicated between this repo's host and the scaffolder's template, where the
9
+ * copies had already drifted — the template's number reader had no
10
+ * "optional, no default" form, so a generated app could not express a ceiling
11
+ * that means "leave the plugin's own default alone".
12
+ *
13
+ * Every reader here **throws rather than guesses**. That is the whole point of
14
+ * the module: the failure mode of environment parsing is silence, and a config
15
+ * value that is quietly wrong is discovered a session TTL later, or by reading
16
+ * a `Set-Cookie` header, rather than at boot with the variable named.
17
+ *
18
+ * The last two exports — {@link when} and {@link defined} — read nothing. They
19
+ * are here because they are what a host does with the readers' `undefined`:
20
+ * plugins merge their settings as `{ ...DEFAULTS, ...config }`, so an unset
21
+ * value must become an *absent key* rather than an explicit `undefined`, which
22
+ * would erase the default it was meant to leave alone. Every host writing that
23
+ * as `...(x ? { key } : {})` at each call site is the duplication this module
24
+ * exists to end.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.NODE_ENVS = void 0;
28
+ exports.readEnv = readEnv;
29
+ exports.requireEnv = requireEnv;
30
+ exports.readPositiveInt = readPositiveInt;
31
+ exports.readOptionalPositiveInt = readOptionalPositiveInt;
32
+ exports.readList = readList;
33
+ exports.readTrustProxy = readTrustProxy;
34
+ exports.readNodeEnv = readNodeEnv;
35
+ exports.isProduction = isProduction;
36
+ exports.readOptionalList = readOptionalList;
37
+ exports.readFlag = readFlag;
38
+ exports.when = when;
39
+ exports.defined = defined;
40
+ /**
41
+ * A trimmed environment value, `undefined` when unset **or empty**.
42
+ *
43
+ * The one reader every other reader here is built on, so "empty means not
44
+ * configured" is decided once. That equivalence matters: `.env.example` ships
45
+ * keys with no value, and `ANTHROPIC_API_KEY=` has to leave a deployment with
46
+ * *no* Claude backend rather than one registered with an empty key — a backend
47
+ * that is in the picker and fails on the first message.
48
+ */
49
+ function readEnv(name) {
50
+ return process.env[name]?.trim() || undefined;
51
+ }
52
+ /**
53
+ * Reads a value the app cannot run without, failing at load rather than
54
+ * several seconds into boot.
55
+ *
56
+ * Left to default to `''`, a missing `DATABASE_URL` reaches `pg` as "use the
57
+ * libpq defaults", and the first thing that touches the database fails with
58
+ * whatever the local libpq environment happens to produce (measured: `SASL:
59
+ * SCRAM-SERVER-FIRST-MESSAGE: client password must be a string`). The server
60
+ * does fail closed, which is the important half, but nothing in that message
61
+ * names the variable that was never set.
62
+ *
63
+ * @param name The environment variable to read.
64
+ * @param hint Appended to the error, for a deployment-specific instruction —
65
+ * which file to copy, which secret store to look in. Omitted, the message
66
+ * says only that there is no usable default.
67
+ */
68
+ function requireEnv(name, hint) {
69
+ const raw = readEnv(name);
70
+ if (!raw) {
71
+ throw new Error(`Missing required environment variable ${name}. ` +
72
+ (hint ?? 'The server has no usable default for this value.'));
73
+ }
74
+ return raw;
75
+ }
76
+ /**
77
+ * Reads a numeric setting: the default when unset or empty, the value when it
78
+ * is a plain positive decimal integer, and an error otherwise.
79
+ *
80
+ * This replaces `Number(process.env[x]) || default`, which was wrong in three
81
+ * directions at once and silent in all of them. `0` is falsy, so it became the
82
+ * default — `LOGIN_RATE_LIMIT=0` ("block every login") quietly meant 10. A
83
+ * negative is truthy, so it was accepted — `SESSION_TTL_SECONDS=-1` issued
84
+ * every session already expired, login answering `201` and the very next
85
+ * request `401`. And exponent notation parsed, so `GRAPHQL_MAX_DEPTH=1e9`
86
+ * removed the cost budget that ADR-0008 calls GraphQL's replacement for REST's
87
+ * structural bound. Refusing to boot names the variable; the alternative was a
88
+ * deployment that looked configured and was not.
89
+ *
90
+ * Empty is deliberately *not* an error: `.env.example` ships several keys with
91
+ * no value, and a fresh clone must boot from it unchanged.
92
+ */
93
+ function readPositiveInt(name, fallback) {
94
+ return readOptionalPositiveInt(name) ?? fallback;
95
+ }
96
+ /**
97
+ * As {@link readPositiveInt}, but `undefined` when unset — no default to fall
98
+ * back to.
99
+ *
100
+ * The form a *ceiling* needs. A plugin that ships its own default cannot be
101
+ * handed `{ maxSteps: undefined }`, because spreading that overwrites the
102
+ * default with nothing; the caller conditionally spreads on this instead.
103
+ */
104
+ function readOptionalPositiveInt(name) {
105
+ const raw = readEnv(name);
106
+ if (!raw) {
107
+ return undefined;
108
+ }
109
+ // Plain decimal digits only. `Number` would also take `1e9`, `0x20` and
110
+ // `Infinity`, none of which anyone means to write in a `.env`.
111
+ if (!/^\d+$/.test(raw) || Number(raw) <= 0) {
112
+ throw new Error(`Environment variable ${name} must be a positive whole number ` +
113
+ `(got "${raw}").`);
114
+ }
115
+ return Number(raw);
116
+ }
117
+ /**
118
+ * A comma-separated list setting, trimmed and emptied of blanks.
119
+ *
120
+ * An explicitly empty value yields an empty list rather than the fallback,
121
+ * because "allow no origins" is a setting somebody means.
122
+ */
123
+ function readList(name, fallback) {
124
+ return (process.env[name] ?? fallback)
125
+ .split(',')
126
+ .map((item) => item.trim())
127
+ .filter(Boolean);
128
+ }
129
+ /**
130
+ * Express's `trust proxy` setting, in the three shapes it accepts.
131
+ *
132
+ * Checked in this order: a hop count (`'1'` — the recommended form, and the
133
+ * only one a client cannot forge past), a boolean (`'true'` trusts the entire
134
+ * `X-Forwarded-For` chain, `'false'` trusts none), or any other non-empty
135
+ * string, passed to Express verbatim as a subnet/preset list (`'loopback'`,
136
+ * `'10.0.0.0/8'`). Unset yields `undefined`, leaving Express's default of
137
+ * ignoring forwarded headers entirely.
138
+ *
139
+ * Returns the union structurally rather than importing `TrustProxySetting`
140
+ * from `@orthacms/bootstrap-server`: this is a leaf helper package, and the
141
+ * host importing it must not become a dependency of it. The host's own
142
+ * `trustProxy?: TrustProxySetting` field is what checks the two agree.
143
+ */
144
+ function readTrustProxy(name = 'TRUST_PROXY') {
145
+ const raw = readEnv(name);
146
+ if (!raw) {
147
+ return undefined;
148
+ }
149
+ const hops = Number(raw);
150
+ if (Number.isInteger(hops) && hops >= 0) {
151
+ return hops;
152
+ }
153
+ if (raw === 'true' || raw === 'false') {
154
+ return raw === 'true';
155
+ }
156
+ return raw;
157
+ }
158
+ /** The deployment modes an Ortha app recognises. */
159
+ exports.NODE_ENVS = ['development', 'test', 'production'];
160
+ /**
161
+ * Reads `NODE_ENV`, rejecting a value that is neither recognised nor empty.
162
+ *
163
+ * `NODE_ENV !== 'production'` is the switch behind **two** protections at once
164
+ * — whether the API reference and GraphiQL are published, and whether the
165
+ * session cookie carries `Secure` — so any value that is not exactly
166
+ * `production` turns both off. Unset is a legitimate, and the common, local
167
+ * state; a *typo* is not, and it is indistinguishable from correct
168
+ * configuration until you read a `Set-Cookie` header. Measured: `produciton`
169
+ * serves `/reference/json` to an unauthenticated caller and drops `Secure`
170
+ * from the session cookie, exactly as if nothing had been set (ORT-137).
171
+ *
172
+ * Rejecting the typo costs a deployment that spells it right nothing, and
173
+ * turns a silent downgrade into a refusal to start.
174
+ */
175
+ function readNodeEnv() {
176
+ const raw = readEnv('NODE_ENV');
177
+ if (!raw) {
178
+ return undefined;
179
+ }
180
+ if (!exports.NODE_ENVS.includes(raw)) {
181
+ throw new Error(`NODE_ENV is "${raw}", which this app does not recognise — expected ` +
182
+ `one of ${exports.NODE_ENVS.join(', ')}, or nothing at all for local ` +
183
+ 'development. Anything else reads as "not production", which ' +
184
+ 'publishes the API reference and drops `Secure` from the session ' +
185
+ 'cookie.');
186
+ }
187
+ return raw;
188
+ }
189
+ /**
190
+ * True only in a deployment that said `production`, with the spelling checked
191
+ * by {@link readNodeEnv}.
192
+ */
193
+ function isProduction() {
194
+ return readNodeEnv() === 'production';
195
+ }
196
+ /**
197
+ * As {@link readList}, but `undefined` when the variable is unset, so the
198
+ * consumer's own default list survives rather than being replaced by an empty
199
+ * one.
200
+ *
201
+ * The list counterpart to {@link readOptionalPositiveInt}, and needed for the
202
+ * same reason: a setting whose owner ships a default cannot be handed an empty
203
+ * array meaning "the deployment said nothing".
204
+ */
205
+ function readOptionalList(name) {
206
+ return when(readEnv(name), () => readList(name, ''));
207
+ }
208
+ /**
209
+ * A boolean setting: what the deployment said, or `fallback` when it said
210
+ * nothing.
211
+ *
212
+ * Anything other than `true` reads as false, so a typo turns a switch **off**
213
+ * rather than on. That direction is deliberate: the switches this reads are
214
+ * kill switches for surfaces that send content to a third party or open a door
215
+ * for an external agent, and a misspelling should not open one.
216
+ */
217
+ function readFlag(name, fallback) {
218
+ const raw = readEnv(name);
219
+ return raw === undefined ? fallback : raw === 'true';
220
+ }
221
+ /**
222
+ * The value when the setting was configured, `undefined` when it was not.
223
+ *
224
+ * The counterpart to {@link defined}: together they replace
225
+ * `...(x ? { key: … } : {})` at a config site. `build` is a thunk so its body —
226
+ * often several further reads — runs only when it applies, which is what lets a
227
+ * whole provider block sit behind one credential check.
228
+ */
229
+ function when(configured, build) {
230
+ return configured ? build() : undefined;
231
+ }
232
+ /**
233
+ * The same object with every `undefined`-valued key removed.
234
+ *
235
+ * A plugin merges its own defaults as `{ ...DEFAULTS, ...config }`, so an
236
+ * explicit `{ maxSteps: undefined }` does not leave `DEFAULT_RUN_LIMITS.maxSteps`
237
+ * in place — it erases it. Dropping the key is what "the deployment did not set
238
+ * this" has to mean, and saying it once at the end of a builder is why a config
239
+ * file no longer needs a conditional per optional setting.
240
+ */
241
+ function defined(value) {
242
+ const result = {};
243
+ for (const [key, item] of Object.entries(value)) {
244
+ if (item !== undefined) {
245
+ result[key] = item;
246
+ }
247
+ }
248
+ return result;
249
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Every ceiling the filter engine imposes, in one place.
3
+ *
4
+ * The engine's job is to accept a tree a stranger wrote. What stops that tree
5
+ * from being unbounded work is this file — nothing else. Four of these budgets
6
+ * count *structure* (how many rules, how deep, how long an `IN` list); the
7
+ * fifth counts *text*, and it was missing.
8
+ *
9
+ * ## What the DTO cap covers, and what it does not
10
+ *
11
+ * {@link FILTER_MAX_LENGTH} is the cap a caller's DTO puts on the whole
12
+ * serialised filter — `@MaxLength(FILTER_MAX_LENGTH)` on `?filter=`. It lives
13
+ * here rather than in each plugin because it was declared separately in four of
14
+ * them and one copy had drifted to 8192, so "the filter cap" meant two numbers
15
+ * depending on which endpoint you asked.
16
+ *
17
+ * It is a **transport** guard, and it only guards the paths where the filter
18
+ * arrives as a string. It does not reach:
19
+ *
20
+ * - **Alarm rules.** `CreateAlarmRuleDto.filter` is an already-parsed object
21
+ * (`@IsObject()`), because the authority on a filter tree's shape is this
22
+ * parser rather than a second copy of the grammar in decorators. `@MaxLength`
23
+ * is a string decorator and could not be applied to it.
24
+ * - **A stored alarm rule replayed from its `jsonb` column** — by the outbox
25
+ * subscriber, the periodic sweep, or `POST /rules/:id/rescan`. Nothing
26
+ * re-serialises it on the way out of the database, so there is no string to
27
+ * measure.
28
+ * - **The copilot's `admin_content_search` tool.** Its `filter` argument is an
29
+ * object too, and the registry's input check deliberately ignores `anyOf`, so
30
+ * the tree's contents reach the parser unexamined.
31
+ * - **GraphQL's entry loader**, which builds an id-`in` tree in code and casts
32
+ * past the DTO.
33
+ *
34
+ * On those four paths the parser *is* the boundary. That is why
35
+ * {@link DEFAULT_MAX_VALUE_LENGTH} exists: a budget declared on the DTO
36
+ * protects the endpoints that happen to have one, and a budget declared here
37
+ * protects every caller there will ever be.
38
+ */
39
+ /**
40
+ * Max length of a serialised filter payload, for the `@MaxLength` a caller
41
+ * puts on its own `?filter=` / `filter` DTO field.
42
+ *
43
+ * A coarse first latch ahead of the engine's own budgets: it refuses an
44
+ * oversized payload before `JSON.parse` sees it. It is deliberately generous —
45
+ * roughly a hundred UUID clauses — because it is not the real bound; the node,
46
+ * depth, list and value budgets below are.
47
+ *
48
+ * **The one number, and every DTO imports it from here.** `activity`,
49
+ * `content` and `users` each declared their own `4096` and `alarms` a `8192`
50
+ * that was applied to nothing at all; four declarations of one rule is three
51
+ * chances to answer a caller differently on two endpoints for the same filter.
52
+ */
53
+ export declare const FILTER_MAX_LENGTH = 4096;
54
+ /** Default `schema.maxDepth` — how many dotted relation hops a path may walk. */
55
+ export declare const DEFAULT_MAX_DEPTH = 3;
56
+ /** Default `schema.maxNodes` — how many groups + rules one tree may hold. */
57
+ export declare const DEFAULT_MAX_NODES = 50;
58
+ /** Default `schema.maxGroupDepth` — how deep `and`/`or` nesting may go. */
59
+ export declare const DEFAULT_MAX_GROUP_DEPTH = 5;
60
+ /** Default `schema.maxInListLength` — how many values one `in`/`nin` may name. */
61
+ export declare const DEFAULT_MAX_IN_LIST = 100;
62
+ /**
63
+ * Default `schema.maxValueLength` — how long one clause's **string** value may
64
+ * be, applied to each element of an `in` list as well as to a bare scalar.
65
+ *
66
+ * The number is {@link FILTER_MAX_LENGTH} on purpose, and the equality is the
67
+ * argument for it: a filter that arrives as a string is already capped at that
68
+ * length in total, so no value inside one can be longer. This budget therefore
69
+ * cannot refuse anything an HTTP caller could already send — it exists for the
70
+ * paths listed at the top of this file, which never meet the DTO at all, and
71
+ * where the alternative bound was the 1 MB body limit.
72
+ *
73
+ * What it stops is a single arbitrarily long `LIKE` pattern: `~~` is scanned
74
+ * against every candidate row, so the cost of one clause is the pattern's
75
+ * length times the table's, and nothing above the parser was measuring the
76
+ * first factor.
77
+ */
78
+ export declare const DEFAULT_MAX_VALUE_LENGTH = 4096;
79
+ //# sourceMappingURL=budgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budgets.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/budgets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,kFAAkF;AAClF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,OAAoB,CAAC"}
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * Every ceiling the filter engine imposes, in one place.
4
+ *
5
+ * The engine's job is to accept a tree a stranger wrote. What stops that tree
6
+ * from being unbounded work is this file — nothing else. Four of these budgets
7
+ * count *structure* (how many rules, how deep, how long an `IN` list); the
8
+ * fifth counts *text*, and it was missing.
9
+ *
10
+ * ## What the DTO cap covers, and what it does not
11
+ *
12
+ * {@link FILTER_MAX_LENGTH} is the cap a caller's DTO puts on the whole
13
+ * serialised filter — `@MaxLength(FILTER_MAX_LENGTH)` on `?filter=`. It lives
14
+ * here rather than in each plugin because it was declared separately in four of
15
+ * them and one copy had drifted to 8192, so "the filter cap" meant two numbers
16
+ * depending on which endpoint you asked.
17
+ *
18
+ * It is a **transport** guard, and it only guards the paths where the filter
19
+ * arrives as a string. It does not reach:
20
+ *
21
+ * - **Alarm rules.** `CreateAlarmRuleDto.filter` is an already-parsed object
22
+ * (`@IsObject()`), because the authority on a filter tree's shape is this
23
+ * parser rather than a second copy of the grammar in decorators. `@MaxLength`
24
+ * is a string decorator and could not be applied to it.
25
+ * - **A stored alarm rule replayed from its `jsonb` column** — by the outbox
26
+ * subscriber, the periodic sweep, or `POST /rules/:id/rescan`. Nothing
27
+ * re-serialises it on the way out of the database, so there is no string to
28
+ * measure.
29
+ * - **The copilot's `admin_content_search` tool.** Its `filter` argument is an
30
+ * object too, and the registry's input check deliberately ignores `anyOf`, so
31
+ * the tree's contents reach the parser unexamined.
32
+ * - **GraphQL's entry loader**, which builds an id-`in` tree in code and casts
33
+ * past the DTO.
34
+ *
35
+ * On those four paths the parser *is* the boundary. That is why
36
+ * {@link DEFAULT_MAX_VALUE_LENGTH} exists: a budget declared on the DTO
37
+ * protects the endpoints that happen to have one, and a budget declared here
38
+ * protects every caller there will ever be.
39
+ */
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.DEFAULT_MAX_VALUE_LENGTH = exports.DEFAULT_MAX_IN_LIST = exports.DEFAULT_MAX_GROUP_DEPTH = exports.DEFAULT_MAX_NODES = exports.DEFAULT_MAX_DEPTH = exports.FILTER_MAX_LENGTH = void 0;
42
+ /**
43
+ * Max length of a serialised filter payload, for the `@MaxLength` a caller
44
+ * puts on its own `?filter=` / `filter` DTO field.
45
+ *
46
+ * A coarse first latch ahead of the engine's own budgets: it refuses an
47
+ * oversized payload before `JSON.parse` sees it. It is deliberately generous —
48
+ * roughly a hundred UUID clauses — because it is not the real bound; the node,
49
+ * depth, list and value budgets below are.
50
+ *
51
+ * **The one number, and every DTO imports it from here.** `activity`,
52
+ * `content` and `users` each declared their own `4096` and `alarms` a `8192`
53
+ * that was applied to nothing at all; four declarations of one rule is three
54
+ * chances to answer a caller differently on two endpoints for the same filter.
55
+ */
56
+ exports.FILTER_MAX_LENGTH = 4096;
57
+ /** Default `schema.maxDepth` — how many dotted relation hops a path may walk. */
58
+ exports.DEFAULT_MAX_DEPTH = 3;
59
+ /** Default `schema.maxNodes` — how many groups + rules one tree may hold. */
60
+ exports.DEFAULT_MAX_NODES = 50;
61
+ /** Default `schema.maxGroupDepth` — how deep `and`/`or` nesting may go. */
62
+ exports.DEFAULT_MAX_GROUP_DEPTH = 5;
63
+ /** Default `schema.maxInListLength` — how many values one `in`/`nin` may name. */
64
+ exports.DEFAULT_MAX_IN_LIST = 100;
65
+ /**
66
+ * Default `schema.maxValueLength` — how long one clause's **string** value may
67
+ * be, applied to each element of an `in` list as well as to a bare scalar.
68
+ *
69
+ * The number is {@link FILTER_MAX_LENGTH} on purpose, and the equality is the
70
+ * argument for it: a filter that arrives as a string is already capped at that
71
+ * length in total, so no value inside one can be longer. This budget therefore
72
+ * cannot refuse anything an HTTP caller could already send — it exists for the
73
+ * paths listed at the top of this file, which never meet the DTO at all, and
74
+ * where the alternative bound was the 1 MB body limit.
75
+ *
76
+ * What it stops is a single arbitrarily long `LIKE` pattern: `~~` is scanned
77
+ * against every candidate row, so the cost of one clause is the pattern's
78
+ * length times the table's, and nothing above the parser was measuring the
79
+ * first factor.
80
+ */
81
+ exports.DEFAULT_MAX_VALUE_LENGTH = exports.FILTER_MAX_LENGTH;
@@ -39,6 +39,13 @@ export declare const FilterErrorCode: {
39
39
  readonly EmptyInList: "FILTER_EMPTY_IN_LIST";
40
40
  /** `in`/`nin` value list exceeded `schema.maxInListLength` (default 100). */
41
41
  readonly MaxInListExceeded: "FILTER_MAX_IN_LIST_EXCEEDED";
42
+ /**
43
+ * A single clause's string value exceeded `schema.maxValueLength`
44
+ * (default 4096) — the engine's only budget over *text* rather than over
45
+ * the tree's structure. Reported per clause, naming the offending path, so
46
+ * a client can point at the rule rather than at the whole filter.
47
+ */
48
+ readonly ValueTooLong: "FILTER_VALUE_TOO_LONG";
42
49
  };
43
50
  /** One of the {@link FilterErrorCode} values. */
44
51
  export type FilterErrorCode = (typeof FilterErrorCode)[keyof typeof FilterErrorCode];
@@ -1 +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"}
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;;IAE7E;;;;;OAKG;;CAEG,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"}
@@ -41,7 +41,14 @@ exports.FilterErrorCode = {
41
41
  */
42
42
  EmptyInList: 'FILTER_EMPTY_IN_LIST',
43
43
  /** `in`/`nin` value list exceeded `schema.maxInListLength` (default 100). */
44
- MaxInListExceeded: 'FILTER_MAX_IN_LIST_EXCEEDED'
44
+ MaxInListExceeded: 'FILTER_MAX_IN_LIST_EXCEEDED',
45
+ /**
46
+ * A single clause's string value exceeded `schema.maxValueLength`
47
+ * (default 4096) — the engine's only budget over *text* rather than over
48
+ * the tree's structure. Reported per clause, naming the offending path, so
49
+ * a client can point at the rule rather than at the whole filter.
50
+ */
51
+ ValueTooLong: 'FILTER_VALUE_TOO_LONG'
45
52
  };
46
53
  /**
47
54
  * Typed 400 exception thrown by the filter parser.
@@ -1 +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"}
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;AAmE1D;;;;;;;;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"}
@@ -47,6 +47,18 @@ const COMPARABLE_OPERATORS = [
47
47
  types_1.FilterOperator.Nin,
48
48
  types_1.FilterOperator.Null
49
49
  ];
50
+ /**
51
+ * {@link COMPARABLE_OPERATORS} plus the relative window.
52
+ *
53
+ * `within_last` is date-only because it compares against `now()` — asking for
54
+ * "titles within the last 7 days" is not a narrower query, it is a category
55
+ * error, and Postgres would report it as a type mismatch rather than as
56
+ * something the client can fix.
57
+ */
58
+ const DATE_OPERATORS = [
59
+ ...COMPARABLE_OPERATORS,
60
+ types_1.FilterOperator.WithinLast
61
+ ];
50
62
  /** {@link COMPARABLE_OPERATORS} plus the text-only `~~` family. */
51
63
  const TEXT_OPERATORS = [
52
64
  ...COMPARABLE_OPERATORS,
@@ -67,7 +79,7 @@ exports.OPERATORS_BY_TYPE = {
67
79
  [types_1.ScalarFieldType.Number]: COMPARABLE_OPERATORS,
68
80
  [types_1.ScalarFieldType.Boolean]: COMPARABLE_OPERATORS,
69
81
  [types_1.ScalarFieldType.Uuid]: COMPARABLE_OPERATORS,
70
- [types_1.ScalarFieldType.Date]: COMPARABLE_OPERATORS
82
+ [types_1.ScalarFieldType.Date]: DATE_OPERATORS
71
83
  };
72
84
  /**
73
85
  * The operators `type` accepts, or `undefined` when the type is not one this
@@ -1 +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"}
1
+ {"version":3,"file":"parse-filter-tree.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/parse-filter-tree.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAgB,UAAU,EAAE,MAAM,SAAS,CAAC;AAEtE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC3B,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,YAAY,GACrB,UAAU,GAAG,IAAI,CAqCnB"}
@@ -1,12 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseFilterTree = parseFilterTree;
4
+ const budgets_1 = require("./budgets");
4
5
  const filter_exceptions_1 = require("./filter-exceptions");
5
6
  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
7
  /**
11
8
  * Parse a `filter` payload into a tree the translator can walk. Two
12
9
  * input shapes are accepted:
@@ -44,10 +41,11 @@ function parseFilterTree(rawFilter, schema) {
44
41
  return null;
45
42
  const ctx = {
46
43
  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
44
+ maxNodes: schema.maxNodes ?? budgets_1.DEFAULT_MAX_NODES,
45
+ maxGroupDepth: schema.maxGroupDepth ?? budgets_1.DEFAULT_MAX_GROUP_DEPTH,
46
+ maxDepth: schema.maxDepth ?? budgets_1.DEFAULT_MAX_DEPTH,
47
+ maxInListLength: schema.maxInListLength ?? budgets_1.DEFAULT_MAX_IN_LIST,
48
+ maxValueLength: schema.maxValueLength ?? budgets_1.DEFAULT_MAX_VALUE_LENGTH
51
49
  };
52
50
  return walkNode(obj, schema, 0, ctx);
53
51
  }
@@ -97,7 +95,7 @@ function walkNode(raw, schema, depth, ctx) {
97
95
  throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'rule `op` must be a non-empty string');
98
96
  }
99
97
  const path = field.split('.');
100
- const leaf = (0, resolve_leaf_1.resolveLeaf)(path, op, node.value, schema, ctx.maxDepth, ctx.maxInListLength);
98
+ const leaf = (0, resolve_leaf_1.resolveLeaf)(path, op, node.value, schema, ctx);
101
99
  return toRule(leaf);
102
100
  }
103
101
  throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidNode, 'tree node must have `and`, `or`, or (`field` + `op`)');
@@ -1,8 +1,21 @@
1
1
  import type { FilterSchema, ParsedFilter } from './types';
2
+ /**
3
+ * The per-leaf budgets, passed as one object rather than as a growing tail of
4
+ * positional numbers — three `number` parameters in a row is a call site where
5
+ * transposing two of them typechecks.
6
+ */
7
+ export interface LeafLimits {
8
+ /** How many dotted relation hops the path may walk. */
9
+ maxDepth: number;
10
+ /** How many values one `in`/`nin` may name. */
11
+ maxInListLength: number;
12
+ /** How long one string value may be — see `budgets.ts`. */
13
+ maxValueLength: number;
14
+ }
2
15
  /**
3
16
  * Walk a single dotted path against the schema, validate the operator,
4
17
  * and coerce the value to the declared type. Produces the `ParsedFilter`
5
18
  * leaf shape the tree parser tags with `kind: 'rule'`.
6
19
  */
7
- export declare function resolveLeaf(path: string[], op: string, value: unknown, schema: FilterSchema, maxDepth: number, maxInListLength: number): ParsedFilter;
20
+ export declare function resolveLeaf(path: string[], op: string, value: unknown, schema: FilterSchema, limits: LeafLimits): ParsedFilter;
8
21
  //# sourceMappingURL=resolve-leaf.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"resolve-leaf.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/resolve-leaf.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACR,YAAY,EACZ,YAAY,EAGf,MAAM,SAAS,CAAC;AAQjB;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACvB,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,eAAe,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,UAAU,GACnB,YAAY,CA6Dd"}
@@ -13,7 +13,8 @@ const UUID_CANONICAL = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f
13
13
  * and coerce the value to the declared type. Produces the `ParsedFilter`
14
14
  * leaf shape the tree parser tags with `kind: 'rule'`.
15
15
  */
16
- function resolveLeaf(path, op, value, schema, maxDepth, maxInListLength) {
16
+ function resolveLeaf(path, op, value, schema, limits) {
17
+ const { maxDepth } = limits;
17
18
  if (path.length === 0) {
18
19
  throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.EmptyPath, 'empty filter path');
19
20
  }
@@ -37,7 +38,7 @@ function resolveLeaf(path, op, value, schema, maxDepth, maxInListLength) {
37
38
  return {
38
39
  path,
39
40
  op: op,
40
- value: coerce(value, field, op, path, maxInListLength)
41
+ value: coerce(value, field, op, path, limits)
41
42
  };
42
43
  }
43
44
  const rel = (0, own_property_1.own)(relations, seg);
@@ -69,8 +70,12 @@ function assertOperatorAllowed(field, op, path) {
69
70
  }
70
71
  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
  }
72
- function coerce(raw, field, op, path, maxInListLength) {
73
+ function coerce(raw, field, op, path, limits) {
74
+ const { maxInListLength } = limits;
73
75
  const pathStr = path.join('.');
76
+ if (op === types_1.FilterOperator.WithinLast) {
77
+ return withinLastValue(raw, pathStr);
78
+ }
74
79
  if (op === types_1.FilterOperator.Null) {
75
80
  if (raw === 'true' || raw === true)
76
81
  return true;
@@ -96,11 +101,39 @@ function coerce(raw, field, op, path, maxInListLength) {
96
101
  if (items.length > maxInListLength) {
97
102
  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
103
  }
99
- return items.map((v) => scalarOf(v, field, pathStr));
104
+ return items.map((v) => scalarOf(v, field, pathStr, limits));
100
105
  }
101
- return scalarOf(raw, field, pathStr);
106
+ return scalarOf(raw, field, pathStr, limits);
102
107
  }
103
- function scalarOf(v, field, pathStr) {
108
+ /** Units a `within_last` window may name, as a set for membership checks. */
109
+ const WITHIN_LAST_UNITS = new Set(Object.values(types_1.WithinLastUnit));
110
+ /**
111
+ * Coerce a `within_last` value — `{ n, unit }`, the only object-shaped value in
112
+ * the grammar.
113
+ *
114
+ * The bound on `n` is not decoration. `now() - make_interval(days => 1e9)`
115
+ * overflows Postgres' timestamp range and raises a `22008` the caller sees as a
116
+ * 500; a client asking for a window that long means to say "all of it", and
117
+ * should be told so with a 400 rather than a stack trace. Ten years of minutes
118
+ * is far past any real window and comfortably inside what a timestamp can hold.
119
+ */
120
+ const MAX_WITHIN_LAST_N = 10_000_000;
121
+ function withinLastValue(raw, pathStr) {
122
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
123
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, 'within_last expects { n, unit }', { path: pathStr, op: types_1.FilterOperator.WithinLast });
124
+ }
125
+ const value = raw;
126
+ const n = Number(value['n']);
127
+ const unit = value['unit'];
128
+ if (!Number.isInteger(n) || n < 1 || n > MAX_WITHIN_LAST_N) {
129
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `within_last n must be an integer between 1 and ${MAX_WITHIN_LAST_N}`, { path: pathStr, op: types_1.FilterOperator.WithinLast, value: value['n'] });
130
+ }
131
+ if (typeof unit !== 'string' || !WITHIN_LAST_UNITS.has(unit)) {
132
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, `within_last unit must be one of ${[...WITHIN_LAST_UNITS].join(', ')}`, { path: pathStr, op: types_1.FilterOperator.WithinLast, value: unit });
133
+ }
134
+ return { n, unit: unit };
135
+ }
136
+ function scalarOf(v, field, pathStr, limits) {
104
137
  // A filter value has to be a scalar. `String(v)` on anything else produces
105
138
  // a plausible-looking string that is then MATCHED AGAINST rather than
106
139
  // rejected: `null` → `"null"`, a missing `value` key → `"undefined"`,
@@ -117,7 +150,24 @@ function scalarOf(v, field, pathStr) {
117
150
  ? 'value is required'
118
151
  : 'value must be a string, number or boolean', { path: pathStr, expectedType: field.type });
119
152
  }
153
+ // The only budget that counts *text*. Every other ceiling in this engine
154
+ // bounds the tree's structure — nodes, hops, group nesting, `in` list
155
+ // length — and a single clause can carry an arbitrarily long string through
156
+ // all of them. On the endpoints that own a DTO the `@MaxLength` on the
157
+ // whole serialised filter caps this first; on the paths that never meet one
158
+ // (an alarm rule replayed out of its `jsonb` column, the copilot's search
159
+ // tool, an internally-built tree) this check is the only bound there is,
160
+ // and the alternative was the 1 MB body limit. Applied to a `number` or
161
+ // `boolean` too, which is free: their `String(v)` is a handful of
162
+ // characters and can never trip it.
120
163
  const s = typeof v === 'string' ? v : String(v);
164
+ if (s.length > limits.maxValueLength) {
165
+ throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.ValueTooLong, `value exceeds max length ${limits.maxValueLength}`, {
166
+ path: pathStr,
167
+ maxValueLength: limits.maxValueLength,
168
+ length: s.length
169
+ });
170
+ }
121
171
  switch (field.type) {
122
172
  case types_1.ScalarFieldType.String:
123
173
  return s;
@@ -1 +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"}
1
+ {"version":3,"file":"scalar-op.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/scalar-op.ts"],"names":[],"mappings":"AAAA,OAAO,EAgBH,KAAK,SAAS,EACd,KAAK,GAAG,EACX,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAC;AAkBzD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAClB,GAAG,EAAE,SAAS,EACd,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,OAAO,GACf,GAAG,CAuCL"}
@@ -54,6 +54,8 @@ function scalar(col, op, value) {
54
54
  return negative(col, (0, drizzle_orm_1.notIlike)(col, String(value)));
55
55
  case types_1.FilterOperator.Null:
56
56
  return value === true ? (0, drizzle_orm_1.isNull)(col) : (0, drizzle_orm_1.isNotNull)(col);
57
+ case types_1.FilterOperator.WithinLast:
58
+ return withinLast(col, value);
57
59
  default:
58
60
  // The parser validates `op` against the vocabulary, so this is
59
61
  // unreachable — but falling out of the switch returned `undefined`,
@@ -64,3 +66,27 @@ function scalar(col, op, value) {
64
66
  throw new filter_exceptions_1.FilterSchemaException(`no translation for operator "${String(op)}"`);
65
67
  }
66
68
  }
69
+ /**
70
+ * `column >= now() - <n> <unit>`.
71
+ *
72
+ * The cutoff is computed by **Postgres, at query time** — which is the whole
73
+ * point of the operator surviving to the server. A cutoff computed here in
74
+ * JavaScript would be identical for one request and wrong for a stored filter
75
+ * replayed a month later.
76
+ *
77
+ * A `switch` over the three units rather than interpolating the unit into the
78
+ * SQL: `make_interval`'s argument names cannot be parameterised, so building
79
+ * the fragment from a variable would mean `sql.raw` on a value that came off
80
+ * the wire. The parser already restricts the unit to these three, and this way
81
+ * there is no path by which that could stop being true.
82
+ */
83
+ function withinLast(col, window) {
84
+ switch (window.unit) {
85
+ case types_1.WithinLastUnit.Minutes:
86
+ return (0, drizzle_orm_1.gte)(col, (0, drizzle_orm_1.sql) `now() - make_interval(mins => ${window.n})`);
87
+ case types_1.WithinLastUnit.Hours:
88
+ return (0, drizzle_orm_1.gte)(col, (0, drizzle_orm_1.sql) `now() - make_interval(hours => ${window.n})`);
89
+ default:
90
+ return (0, drizzle_orm_1.gte)(col, (0, drizzle_orm_1.sql) `now() - make_interval(days => ${window.n})`);
91
+ }
92
+ }
@@ -25,6 +25,20 @@ export declare const FilterOperator: {
25
25
  readonly Ilike: "ilike";
26
26
  readonly Nilike: "nilike";
27
27
  readonly Null: "null";
28
+ /**
29
+ * `column >= now() - <n> <unit>` — a window measured from **query time**,
30
+ * not from the moment the filter was written.
31
+ *
32
+ * The distinction is invisible in a URL and decisive in a stored one. The
33
+ * admin's query builder resolves its own `within_last` into a concrete
34
+ * `gte` cutoff when it serialises a filter into a link, deliberately: a
35
+ * shared deep link should keep showing the same rows. A filter that is
36
+ * *stored and replayed* — an alarm rule — must mean the opposite, or
37
+ * "not updated in 90 days" silently becomes "not updated since the day the
38
+ * rule was written". So the operator survives to the server, and the caller
39
+ * chooses which meaning it wants.
40
+ */
41
+ readonly WithinLast: "within_last";
28
42
  };
29
43
  /** Standard REST operator names. Translator maps each to a Drizzle helper. */
30
44
  export type FilterOperator = (typeof FilterOperator)[keyof typeof FilterOperator];
@@ -188,6 +202,31 @@ export interface FilterSchema {
188
202
  * elements inside one rule's value list).
189
203
  */
190
204
  maxInListLength?: number;
205
+ /**
206
+ * Longest string one clause's value may be — applied to each element of an
207
+ * `in` list as well as to a bare scalar. Defaults to 4096.
208
+ *
209
+ * The engine's only ceiling over **text**. Every other budget here bounds
210
+ * the tree's *structure*, and a caller that never passes a DTO (an alarm
211
+ * rule replayed out of storage, the copilot's search tool) can carry a
212
+ * megabyte-long `LIKE` pattern through all of them.
213
+ */
214
+ maxValueLength?: number;
215
+ }
216
+ /** Time units a {@link FilterOperator.WithinLast} window may be measured in. */
217
+ export declare const WithinLastUnit: {
218
+ readonly Minutes: "minutes";
219
+ readonly Hours: "hours";
220
+ readonly Days: "days";
221
+ };
222
+ /** One of the {@link WithinLastUnit} values. */
223
+ export type WithinLastUnit = (typeof WithinLastUnit)[keyof typeof WithinLastUnit];
224
+ /** The coerced value of a `within_last` leaf. */
225
+ export interface WithinLastValue {
226
+ /** How many units back the window reaches. A positive integer. */
227
+ n: number;
228
+ /** The unit `n` is counted in. */
229
+ unit: WithinLastUnit;
191
230
  }
192
231
  /** Parser output, one node per URL filter entry. */
193
232
  export interface ParsedFilter {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RelationKind = exports.ScalarFieldType = exports.FilterOperator = void 0;
3
+ exports.WithinLastUnit = exports.RelationKind = exports.ScalarFieldType = exports.FilterOperator = void 0;
4
4
  /** Named constants for `FilterOperator` — use in switches and comparisons. */
5
5
  exports.FilterOperator = {
6
6
  Eq: 'eq',
@@ -14,7 +14,21 @@ exports.FilterOperator = {
14
14
  Like: 'like',
15
15
  Ilike: 'ilike',
16
16
  Nilike: 'nilike',
17
- Null: 'null'
17
+ Null: 'null',
18
+ /**
19
+ * `column >= now() - <n> <unit>` — a window measured from **query time**,
20
+ * not from the moment the filter was written.
21
+ *
22
+ * The distinction is invisible in a URL and decisive in a stored one. The
23
+ * admin's query builder resolves its own `within_last` into a concrete
24
+ * `gte` cutoff when it serialises a filter into a link, deliberately: a
25
+ * shared deep link should keep showing the same rows. A filter that is
26
+ * *stored and replayed* — an alarm rule — must mean the opposite, or
27
+ * "not updated in 90 days" silently becomes "not updated since the day the
28
+ * rule was written". So the operator survives to the server, and the caller
29
+ * chooses which meaning it wants.
30
+ */
31
+ WithinLast: 'within_last'
18
32
  };
19
33
  /** Named constants for `ScalarFieldType` — use in switches. */
20
34
  exports.ScalarFieldType = {
@@ -33,3 +47,9 @@ exports.RelationKind = {
33
47
  ManyToMany: 'many-to-many',
34
48
  SelfReferential: 'self-referential'
35
49
  };
50
+ /** Time units a {@link FilterOperator.WithinLast} window may be measured in. */
51
+ exports.WithinLastUnit = {
52
+ Minutes: 'minutes',
53
+ Hours: 'hours',
54
+ Days: 'days'
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/utils-server",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "@orthacms/utils-server — part of Ortha CMS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/utils/server",