@orthacms/utils-server 0.4.2 → 0.4.3
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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/lib/env.d.ts +166 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +249 -0
- package/dist/lib/filters/operator-support.d.ts.map +1 -1
- package/dist/lib/filters/operator-support.js +13 -1
- package/dist/lib/filters/resolve-leaf.d.ts.map +1 -1
- package/dist/lib/filters/resolve-leaf.js +31 -0
- package/dist/lib/filters/scalar-op.d.ts.map +1 -1
- package/dist/lib/filters/scalar-op.js +26 -0
- package/dist/lib/filters/types.d.ts +29 -0
- package/dist/lib/filters/types.d.ts.map +1 -1
- package/dist/lib/filters/types.js +22 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { parseFilterTree } from './lib/filters/parse-filter-tree';
|
|
2
2
|
export { applyFilterTree, type ApplyFilterTreeOptions, type FilterExtensionResolver } from './lib/filters/tree-to-drizzle';
|
|
3
3
|
export type { DbLike, TableLike } from './lib/filters/table-helpers';
|
|
4
|
-
export { FilterOperator, ScalarFieldType, RelationKind } from './lib/filters/types';
|
|
4
|
+
export { FilterOperator, ScalarFieldType, RelationKind, WithinLastUnit } from './lib/filters/types';
|
|
5
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';
|
|
6
|
+
export type { WithinLastValue, ScalarFieldSchema, FieldSchema, RelationSchema, RelationScope, FilterSchema, ParsedRule, ParsedGroup, ParsedNode } from './lib/filters/types';
|
|
7
7
|
export { FilterException, FilterErrorCode, FilterSchemaException } from './lib/filters/filter-exceptions';
|
|
8
8
|
export { clampInt } from './lib/clamp-int';
|
|
9
9
|
export { isForeignKeyViolation, isUniqueViolation, violatedConstraint } from './lib/pg-errors';
|
|
10
|
+
export { readEnv, requireEnv, readPositiveInt, readOptionalPositiveInt, readList, readOptionalList, readFlag, readTrustProxy, readNodeEnv, isProduction, when, defined, NODE_ENVS, type NodeEnv } from './lib/env';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,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,6 +1,6 @@
|
|
|
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.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
6
|
var tree_to_drizzle_1 = require("./lib/filters/tree-to-drizzle");
|
|
@@ -9,6 +9,7 @@ var types_1 = require("./lib/filters/types");
|
|
|
9
9
|
Object.defineProperty(exports, "FilterOperator", { enumerable: true, get: function () { return types_1.FilterOperator; } });
|
|
10
10
|
Object.defineProperty(exports, "ScalarFieldType", { enumerable: true, get: function () { return types_1.ScalarFieldType; } });
|
|
11
11
|
Object.defineProperty(exports, "RelationKind", { enumerable: true, get: function () { return types_1.RelationKind; } });
|
|
12
|
+
Object.defineProperty(exports, "WithinLastUnit", { enumerable: true, get: function () { return types_1.WithinLastUnit; } });
|
|
12
13
|
var operator_support_1 = require("./lib/filters/operator-support");
|
|
13
14
|
Object.defineProperty(exports, "OPERATORS_BY_TYPE", { enumerable: true, get: function () { return operator_support_1.OPERATORS_BY_TYPE; } });
|
|
14
15
|
Object.defineProperty(exports, "operatorsFor", { enumerable: true, get: function () { return operator_support_1.operatorsFor; } });
|
|
@@ -22,3 +23,17 @@ var pg_errors_1 = require("./lib/pg-errors");
|
|
|
22
23
|
Object.defineProperty(exports, "isForeignKeyViolation", { enumerable: true, get: function () { return pg_errors_1.isForeignKeyViolation; } });
|
|
23
24
|
Object.defineProperty(exports, "isUniqueViolation", { enumerable: true, get: function () { return pg_errors_1.isUniqueViolation; } });
|
|
24
25
|
Object.defineProperty(exports, "violatedConstraint", { enumerable: true, get: function () { return pg_errors_1.violatedConstraint; } });
|
|
26
|
+
var env_1 = require("./lib/env");
|
|
27
|
+
Object.defineProperty(exports, "readEnv", { enumerable: true, get: function () { return env_1.readEnv; } });
|
|
28
|
+
Object.defineProperty(exports, "requireEnv", { enumerable: true, get: function () { return env_1.requireEnv; } });
|
|
29
|
+
Object.defineProperty(exports, "readPositiveInt", { enumerable: true, get: function () { return env_1.readPositiveInt; } });
|
|
30
|
+
Object.defineProperty(exports, "readOptionalPositiveInt", { enumerable: true, get: function () { return env_1.readOptionalPositiveInt; } });
|
|
31
|
+
Object.defineProperty(exports, "readList", { enumerable: true, get: function () { return env_1.readList; } });
|
|
32
|
+
Object.defineProperty(exports, "readOptionalList", { enumerable: true, get: function () { return env_1.readOptionalList; } });
|
|
33
|
+
Object.defineProperty(exports, "readFlag", { enumerable: true, get: function () { return env_1.readFlag; } });
|
|
34
|
+
Object.defineProperty(exports, "readTrustProxy", { enumerable: true, get: function () { return env_1.readTrustProxy; } });
|
|
35
|
+
Object.defineProperty(exports, "readNodeEnv", { enumerable: true, get: function () { return env_1.readNodeEnv; } });
|
|
36
|
+
Object.defineProperty(exports, "isProduction", { enumerable: true, get: function () { return env_1.isProduction; } });
|
|
37
|
+
Object.defineProperty(exports, "when", { enumerable: true, get: function () { return env_1.when; } });
|
|
38
|
+
Object.defineProperty(exports, "defined", { enumerable: true, get: function () { return env_1.defined; } });
|
|
39
|
+
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"}
|
package/dist/lib/env.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
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]:
|
|
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":"resolve-leaf.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/resolve-leaf.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,
|
|
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,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"}
|
|
@@ -71,6 +71,9 @@ function assertOperatorAllowed(field, op, path) {
|
|
|
71
71
|
}
|
|
72
72
|
function coerce(raw, field, op, path, maxInListLength) {
|
|
73
73
|
const pathStr = path.join('.');
|
|
74
|
+
if (op === types_1.FilterOperator.WithinLast) {
|
|
75
|
+
return withinLastValue(raw, pathStr);
|
|
76
|
+
}
|
|
74
77
|
if (op === types_1.FilterOperator.Null) {
|
|
75
78
|
if (raw === 'true' || raw === true)
|
|
76
79
|
return true;
|
|
@@ -100,6 +103,34 @@ function coerce(raw, field, op, path, maxInListLength) {
|
|
|
100
103
|
}
|
|
101
104
|
return scalarOf(raw, field, pathStr);
|
|
102
105
|
}
|
|
106
|
+
/** Units a `within_last` window may name, as a set for membership checks. */
|
|
107
|
+
const WITHIN_LAST_UNITS = new Set(Object.values(types_1.WithinLastUnit));
|
|
108
|
+
/**
|
|
109
|
+
* Coerce a `within_last` value — `{ n, unit }`, the only object-shaped value in
|
|
110
|
+
* the grammar.
|
|
111
|
+
*
|
|
112
|
+
* The bound on `n` is not decoration. `now() - make_interval(days => 1e9)`
|
|
113
|
+
* overflows Postgres' timestamp range and raises a `22008` the caller sees as a
|
|
114
|
+
* 500; a client asking for a window that long means to say "all of it", and
|
|
115
|
+
* should be told so with a 400 rather than a stack trace. Ten years of minutes
|
|
116
|
+
* is far past any real window and comfortably inside what a timestamp can hold.
|
|
117
|
+
*/
|
|
118
|
+
const MAX_WITHIN_LAST_N = 10_000_000;
|
|
119
|
+
function withinLastValue(raw, pathStr) {
|
|
120
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
121
|
+
throw new filter_exceptions_1.FilterException(filter_exceptions_1.FilterErrorCode.InvalidValue, 'within_last expects { n, unit }', { path: pathStr, op: types_1.FilterOperator.WithinLast });
|
|
122
|
+
}
|
|
123
|
+
const value = raw;
|
|
124
|
+
const n = Number(value['n']);
|
|
125
|
+
const unit = value['unit'];
|
|
126
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_WITHIN_LAST_N) {
|
|
127
|
+
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'] });
|
|
128
|
+
}
|
|
129
|
+
if (typeof unit !== 'string' || !WITHIN_LAST_UNITS.has(unit)) {
|
|
130
|
+
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 });
|
|
131
|
+
}
|
|
132
|
+
return { n, unit: unit };
|
|
133
|
+
}
|
|
103
134
|
function scalarOf(v, field, pathStr) {
|
|
104
135
|
// A filter value has to be a scalar. `String(v)` on anything else produces
|
|
105
136
|
// a plausible-looking string that is then MATCHED AGAINST rather than
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scalar-op.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/scalar-op.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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];
|
|
@@ -189,6 +203,21 @@ export interface FilterSchema {
|
|
|
189
203
|
*/
|
|
190
204
|
maxInListLength?: number;
|
|
191
205
|
}
|
|
206
|
+
/** Time units a {@link FilterOperator.WithinLast} window may be measured in. */
|
|
207
|
+
export declare const WithinLastUnit: {
|
|
208
|
+
readonly Minutes: "minutes";
|
|
209
|
+
readonly Hours: "hours";
|
|
210
|
+
readonly Days: "days";
|
|
211
|
+
};
|
|
212
|
+
/** One of the {@link WithinLastUnit} values. */
|
|
213
|
+
export type WithinLastUnit = (typeof WithinLastUnit)[keyof typeof WithinLastUnit];
|
|
214
|
+
/** The coerced value of a `within_last` leaf. */
|
|
215
|
+
export interface WithinLastValue {
|
|
216
|
+
/** How many units back the window reaches. A positive integer. */
|
|
217
|
+
n: number;
|
|
218
|
+
/** The unit `n` is counted in. */
|
|
219
|
+
unit: WithinLastUnit;
|
|
220
|
+
}
|
|
192
221
|
/** Parser output, one node per URL filter entry. */
|
|
193
222
|
export interface ParsedFilter {
|
|
194
223
|
/** Dotted path split into segments, e.g. `['workspaces','name']`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/filters/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,GAAG,GAAG,SAAS,CAAC;AAEnE,8EAA8E;AAC9E,eAAO,MAAM,cAAc;;;;;;;;;;;;;
|
|
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