@astram/engine 0.6.0 → 0.8.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/adapters/mariadb-audit-sink.d.ts +10 -1
- package/dist/adapters/mariadb-audit-sink.d.ts.map +1 -1
- package/dist/adapters/mariadb-audit-sink.js +8 -1
- package/dist/decorators/object-type.decorator.d.ts +10 -0
- package/dist/decorators/object-type.decorator.d.ts.map +1 -1
- package/dist/decorators/object-type.decorator.js +5 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -10
- package/dist/module/engine.module.d.ts +8 -0
- package/dist/module/engine.module.d.ts.map +1 -1
- package/dist/module/engine.module.js +7 -4
- package/dist/pipe/context.d.ts +8 -0
- package/dist/pipe/context.d.ts.map +1 -1
- package/dist/pipe/filter-sql.d.ts +66 -0
- package/dist/pipe/filter-sql.d.ts.map +1 -0
- package/dist/pipe/filter-sql.js +266 -0
- package/dist/pipe/record-engine.d.ts +34 -1
- package/dist/pipe/record-engine.d.ts.map +1 -1
- package/dist/pipe/record-engine.js +256 -87
- package/dist/pipe/records-service.d.ts +19 -1
- package/dist/pipe/records-service.d.ts.map +1 -1
- package/dist/pipe/records-service.js +32 -0
- package/dist/pipe/stages/audit.d.ts.map +1 -1
- package/dist/pipe/stages/audit.js +8 -1
- package/dist/pipe/stages/fetch-record.d.ts +11 -1
- package/dist/pipe/stages/fetch-record.d.ts.map +1 -1
- package/dist/pipe/stages/fetch-record.js +18 -9
- package/dist/pipe/stages/permission-gate.d.ts +8 -0
- package/dist/pipe/stages/permission-gate.d.ts.map +1 -1
- package/dist/pipe/stages/permission-gate.js +15 -9
- package/dist/pipe/stages/validate.d.ts +6 -0
- package/dist/pipe/stages/validate.d.ts.map +1 -1
- package/dist/pipe/stages/validate.js +22 -8
- package/dist/registry.d.ts +38 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +128 -2
- package/dist/types.d.ts +133 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/package.json +2 -2
|
@@ -9,15 +9,24 @@
|
|
|
9
9
|
* resolves ids to labels (no raw ids to end users). Sensitive values are never put
|
|
10
10
|
* in the event by the pipe (the audit stage carries only op + typeKey attributes).
|
|
11
11
|
*/
|
|
12
|
+
import { type FrameworkSchemas } from '@astram/core';
|
|
12
13
|
import type { AuditEvent, AuditPort, QueryFn } from '../ports.js';
|
|
13
14
|
export interface AuditSinkConfig {
|
|
14
|
-
/** Schema holding the audit_log table
|
|
15
|
+
/** Schema holding the audit_log table. Wins over `schemas`. */
|
|
15
16
|
readonly schema?: string;
|
|
17
|
+
/** Resolved framework names; the sink uses `schemas.audit` when `schema` is absent. */
|
|
18
|
+
readonly schemas?: Pick<FrameworkSchemas, 'audit'>;
|
|
16
19
|
/** Table name (default audit_log). */
|
|
17
20
|
readonly table?: string;
|
|
18
21
|
}
|
|
19
22
|
export declare class MariaDbAuditSink implements AuditPort {
|
|
20
23
|
private readonly ref;
|
|
24
|
+
/**
|
|
25
|
+
* Default schema (configurable-framework-db, spec 5.B): `config.schema`, else
|
|
26
|
+
* `config.schemas.audit`, else the audit name resolved from `SHIELD_DB_NAMESPACE` once, here at
|
|
27
|
+
* construction (boot), never per event. Unset namespace ⇒ today's default audit database. An
|
|
28
|
+
* invalid namespace throws (fail closed).
|
|
29
|
+
*/
|
|
21
30
|
constructor(config?: AuditSinkConfig);
|
|
22
31
|
record(event: AuditEvent, query: QueryFn): Promise<void>;
|
|
23
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-audit-sink.d.ts","sourceRoot":"","sources":["../../src/adapters/mariadb-audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B
|
|
1
|
+
{"version":3,"file":"mariadb-audit-sink.d.ts","sourceRoot":"","sources":["../../src/adapters/mariadb-audit-sink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;IACnD,sCAAsC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B;;;;;OAKG;gBACS,MAAM,GAAE,eAAoB;IAOlC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB/D"}
|
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.MariaDbAuditSink = void 0;
|
|
15
15
|
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const core_1 = require("@astram/core");
|
|
16
17
|
class MariaDbAuditSink {
|
|
17
18
|
ref;
|
|
19
|
+
/**
|
|
20
|
+
* Default schema (configurable-framework-db, spec 5.B): `config.schema`, else
|
|
21
|
+
* `config.schemas.audit`, else the audit name resolved from `SHIELD_DB_NAMESPACE` once, here at
|
|
22
|
+
* construction (boot), never per event. Unset namespace ⇒ today's default audit database. An
|
|
23
|
+
* invalid namespace throws (fail closed).
|
|
24
|
+
*/
|
|
18
25
|
constructor(config = {}) {
|
|
19
|
-
const schema = config.schema ??
|
|
26
|
+
const schema = config.schema ?? config.schemas?.audit ?? (0, core_1.resolveFrameworkSchemas)(process.env).audit;
|
|
20
27
|
const table = config.table ?? 'audit_log';
|
|
21
28
|
this.ref = `\`${schema}\`.\`${table}\``;
|
|
22
29
|
}
|
|
@@ -51,6 +51,16 @@ export interface ObjectTypeOptions {
|
|
|
51
51
|
* omit-when-absent discipline as `junctions`/`extensible`. See {@link ReadScope}.
|
|
52
52
|
*/
|
|
53
53
|
readonly readScope?: ReadScope;
|
|
54
|
+
/**
|
|
55
|
+
* OPTIONAL extra actions beyond CRUD (ADR-0038). Reflected AS-IS, omit-when-absent; the
|
|
56
|
+
* REGISTRY validates them at boot. See {@link ObjectTypeContract.actions}.
|
|
57
|
+
*/
|
|
58
|
+
readonly actions?: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* OPTIONAL CRUD opt-out (default `true`; `false` for authority-only types, ADR-0038).
|
|
61
|
+
* Reflected AS-IS, omit-when-absent (a defined `false` is kept).
|
|
62
|
+
*/
|
|
63
|
+
readonly crudActions?: boolean;
|
|
54
64
|
}
|
|
55
65
|
/** Per-member options for `@Field` (the field's `key` is read from the member name). */
|
|
56
66
|
export interface FieldOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object-type.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/object-type.decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EACV,iBAAiB,EAEjB,SAAS,EACT,YAAY,EACZ,YAAY,EAKZ,UAAU,IAAI,kBAAkB,EAChC,UAAU,EACV,SAAS,EACT,aAAa,EACd,MAAM,aAAa,CAAC;AAarB;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAI3C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAIlE;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAW,SAAQ,kBAAkB;CAAG;AAEzD;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,iBAAiB,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAIlE;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"object-type.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/object-type.decorator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EACV,iBAAiB,EAEjB,SAAS,EACT,YAAY,EACZ,YAAY,EAKZ,UAAU,IAAI,kBAAkB,EAChC,UAAU,EACV,SAAS,EACT,aAAa,EACd,MAAM,aAAa,CAAC;AAarB;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAI3C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAIlE;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,UAAW,SAAQ,kBAAkB;CAAG;AAEzD;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,iBAAiB,CAO3D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAIlE;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,kBAAkB,CAkCnE"}
|
|
@@ -149,5 +149,10 @@ function reflectObjectType(cls) {
|
|
|
149
149
|
// type without it omits the key (matching the literal), so `deepStrictEqual` holds.
|
|
150
150
|
if (opts.readScope !== undefined)
|
|
151
151
|
out.readScope = opts.readScope;
|
|
152
|
+
// `actions` / `crudActions` (ADR-0038): same omit-when-absent rule; a defined `false` is kept.
|
|
153
|
+
if (opts.actions !== undefined)
|
|
154
|
+
out.actions = opts.actions;
|
|
155
|
+
if (opts.crudActions !== undefined)
|
|
156
|
+
out.crudActions = opts.crudActions;
|
|
152
157
|
return out;
|
|
153
158
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
* - The raw unit of work (`MariaDbUnitOfWork`, it owns a pool) is NOT here: it is on
|
|
18
18
|
* `@astram/engine/internal`, which only the composition root may import.
|
|
19
19
|
*/
|
|
20
|
-
export type { RecordEngine, AccessDeclaration, FieldSpec, FieldType, TrustedSource, IdentityRole, PkStrategy, JunctionSpec, RecordInput, RecordId, RecordView, ListQuery, RecordPage, Principal, ReadScope, ReadScopeCreateMode, } from './types.js';
|
|
20
|
+
export type { RecordEngine, AccessDeclaration, FieldSpec, FieldType, TrustedSource, IdentityRole, PkStrategy, JunctionSpec, RecordInput, RecordId, RecordView, ListQuery, FilterOperator, FilterValue, WhereClause, SearchQuery, RecordPage, AggregateFn, AggregateMeasure, AggregateQuery, AggregateGroup, AggregateResult, Principal, ReadScope, ReadScopeCreateMode, } from './types.js';
|
|
21
21
|
export { READ_SCOPE_NODES_TOKEN } from './types.js';
|
|
22
|
+
export { FRAMEWORK_SCHEMA } from './types.js';
|
|
23
|
+
export { resolveObjectTypeSchema, type FrameworkSchemaNames } from './registry.js';
|
|
22
24
|
export { canonicalUuid } from './uuid.js';
|
|
23
25
|
export { EngineError, CapabilityNotSupportedError, RegistrationError, AuthorizationDeniedError, ValidationError, EngineUnavailableError, } from './errors.js';
|
|
24
26
|
export { ObjectType, Field, PrimaryKey, reflectObjectType, type ObjectTypeOptions, type FieldOptions, type PrimaryKeyOptions, } from './decorators/object-type.decorator.js';
|
|
25
27
|
export { AccessBundle, reflectAccessBundle, type AccessBundleOptions, type AccessBundlePermission, type AccessBundleSpec, } from './decorators/access-bundle.decorator.js';
|
|
26
28
|
export { EngineModule, RECORD_ENGINE, ENGINE_ALL_REGISTRATIONS, ENGINE_ALL_ACCESS_BUNDLES, type EngineModuleOptions, type ObjectTypeInput, } from './module/engine.module.js';
|
|
27
|
-
export { TableRegistry, columnOf, uuidPkColumn, isOwned, ownerColumn, tenantColumnOf, columnForRole, requireRole, } from './registry.js';
|
|
29
|
+
export { TableRegistry, columnOf, uuidPkColumn, isOwned, ownerColumn, tenantColumnOf, columnForRole, requireRole, effectiveActions, } from './registry.js';
|
|
28
30
|
export { RecordEngineImpl, type RecordEngineDeps } from './pipe/record-engine.js';
|
|
29
31
|
export { RecordsService, BoundRecords, type ServiceAccountResolver, SERVICE_ACCOUNT_RESOLVER, } from './pipe/records-service.js';
|
|
30
32
|
export type { EngineDbConfig } from './adapters/mariadb-unit-of-work.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,UAAU,EACV,SAAS,EACT,UAAU,EACV,SAAS,EACT,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,UAAU,EACV,SAAS,EACT,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,EACf,SAAS,EACT,SAAS,EACT,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAKnF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EACL,WAAW,EACX,2BAA2B,EAC3B,iBAAiB,EACjB,wBAAwB,EACxB,eAAe,EACf,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAiBrB,OAAO,EACL,UAAU,EACV,KAAK,EACL,UAAU,EACV,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACvB,MAAM,uCAAuC,CAAC;AAO/C,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,GACtB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,wBAAwB,EACxB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EACL,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,WAAW,EACX,cAAc,EACd,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,sBAAsB,EAC3B,wBAAwB,GACzB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAC1F,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -19,12 +19,20 @@
|
|
|
19
19
|
* `@astram/engine/internal`, which only the composition root may import.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.MariaDbAuditSink = exports.SERVICE_ACCOUNT_RESOLVER = exports.BoundRecords = exports.RecordsService = exports.RecordEngineImpl = exports.requireRole = exports.columnForRole = exports.tenantColumnOf = exports.ownerColumn = exports.isOwned = exports.uuidPkColumn = exports.columnOf = exports.TableRegistry = exports.ENGINE_ALL_ACCESS_BUNDLES = exports.ENGINE_ALL_REGISTRATIONS = exports.RECORD_ENGINE = exports.EngineModule = exports.reflectAccessBundle = exports.AccessBundle = exports.reflectObjectType = exports.PrimaryKey = exports.Field = exports.ObjectType = exports.EngineUnavailableError = exports.ValidationError = exports.AuthorizationDeniedError = exports.RegistrationError = exports.CapabilityNotSupportedError = exports.EngineError = exports.canonicalUuid = exports.READ_SCOPE_NODES_TOKEN = void 0;
|
|
22
|
+
exports.MariaDbAuditSink = exports.SERVICE_ACCOUNT_RESOLVER = exports.BoundRecords = exports.RecordsService = exports.RecordEngineImpl = exports.effectiveActions = exports.requireRole = exports.columnForRole = exports.tenantColumnOf = exports.ownerColumn = exports.isOwned = exports.uuidPkColumn = exports.columnOf = exports.TableRegistry = exports.ENGINE_ALL_ACCESS_BUNDLES = exports.ENGINE_ALL_REGISTRATIONS = exports.RECORD_ENGINE = exports.EngineModule = exports.reflectAccessBundle = exports.AccessBundle = exports.reflectObjectType = exports.PrimaryKey = exports.Field = exports.ObjectType = exports.EngineUnavailableError = exports.ValidationError = exports.AuthorizationDeniedError = exports.RegistrationError = exports.CapabilityNotSupportedError = exports.EngineError = exports.canonicalUuid = exports.resolveObjectTypeSchema = exports.FRAMEWORK_SCHEMA = exports.READ_SCOPE_NODES_TOKEN = void 0;
|
|
23
23
|
// The read-scope subtree-id placeholder token (ADR-0027) — a `subtreeSubquery` template
|
|
24
24
|
// uses it; the engine expands it to a `?`-list. Exported so per-type declarations can
|
|
25
25
|
// reference the canonical token (rather than a stringly-typed duplicate).
|
|
26
26
|
var types_js_1 = require("./types.js");
|
|
27
27
|
Object.defineProperty(exports, "READ_SCOPE_NODES_TOKEN", { enumerable: true, get: function () { return types_js_1.READ_SCOPE_NODES_TOKEN; } });
|
|
28
|
+
// Framework database placeholder tokens (configurable-framework-db, ADR-0037): a framework object
|
|
29
|
+
// type declares `schema: FRAMEWORK_SCHEMA.app` (and may use the tokens in `readScope.subquerySql`);
|
|
30
|
+
// the engine resolves them at registration. Any other reader of an object type's schema calls
|
|
31
|
+
// `resolveObjectTypeSchema(reg, schemas)` before building SQL; an unresolved token throws.
|
|
32
|
+
var types_js_2 = require("./types.js");
|
|
33
|
+
Object.defineProperty(exports, "FRAMEWORK_SCHEMA", { enumerable: true, get: function () { return types_js_2.FRAMEWORK_SCHEMA; } });
|
|
34
|
+
var registry_js_1 = require("./registry.js");
|
|
35
|
+
Object.defineProperty(exports, "resolveObjectTypeSchema", { enumerable: true, get: function () { return registry_js_1.resolveObjectTypeSchema; } });
|
|
28
36
|
// The ONE uuid canonicaliser (Phase 11 #1): strict 8-4-4-4-12 ASCII hex, returned lower-cased;
|
|
29
37
|
// anything else (padded, fullwidth, ...) is null. Guard's `UuidPipe` and the identity body checks
|
|
30
38
|
// use it; consumers may too.
|
|
@@ -71,15 +79,16 @@ Object.defineProperty(exports, "RECORD_ENGINE", { enumerable: true, get: functio
|
|
|
71
79
|
Object.defineProperty(exports, "ENGINE_ALL_REGISTRATIONS", { enumerable: true, get: function () { return engine_module_js_1.ENGINE_ALL_REGISTRATIONS; } });
|
|
72
80
|
Object.defineProperty(exports, "ENGINE_ALL_ACCESS_BUNDLES", { enumerable: true, get: function () { return engine_module_js_1.ENGINE_ALL_ACCESS_BUNDLES; } });
|
|
73
81
|
// --- Framework-facing (composition + tests; NOT for feature code) ------------
|
|
74
|
-
var
|
|
75
|
-
Object.defineProperty(exports, "TableRegistry", { enumerable: true, get: function () { return
|
|
76
|
-
Object.defineProperty(exports, "columnOf", { enumerable: true, get: function () { return
|
|
77
|
-
Object.defineProperty(exports, "uuidPkColumn", { enumerable: true, get: function () { return
|
|
78
|
-
Object.defineProperty(exports, "isOwned", { enumerable: true, get: function () { return
|
|
79
|
-
Object.defineProperty(exports, "ownerColumn", { enumerable: true, get: function () { return
|
|
80
|
-
Object.defineProperty(exports, "tenantColumnOf", { enumerable: true, get: function () { return
|
|
81
|
-
Object.defineProperty(exports, "columnForRole", { enumerable: true, get: function () { return
|
|
82
|
-
Object.defineProperty(exports, "requireRole", { enumerable: true, get: function () { return
|
|
82
|
+
var registry_js_2 = require("./registry.js");
|
|
83
|
+
Object.defineProperty(exports, "TableRegistry", { enumerable: true, get: function () { return registry_js_2.TableRegistry; } });
|
|
84
|
+
Object.defineProperty(exports, "columnOf", { enumerable: true, get: function () { return registry_js_2.columnOf; } });
|
|
85
|
+
Object.defineProperty(exports, "uuidPkColumn", { enumerable: true, get: function () { return registry_js_2.uuidPkColumn; } });
|
|
86
|
+
Object.defineProperty(exports, "isOwned", { enumerable: true, get: function () { return registry_js_2.isOwned; } });
|
|
87
|
+
Object.defineProperty(exports, "ownerColumn", { enumerable: true, get: function () { return registry_js_2.ownerColumn; } });
|
|
88
|
+
Object.defineProperty(exports, "tenantColumnOf", { enumerable: true, get: function () { return registry_js_2.tenantColumnOf; } });
|
|
89
|
+
Object.defineProperty(exports, "columnForRole", { enumerable: true, get: function () { return registry_js_2.columnForRole; } });
|
|
90
|
+
Object.defineProperty(exports, "requireRole", { enumerable: true, get: function () { return registry_js_2.requireRole; } });
|
|
91
|
+
Object.defineProperty(exports, "effectiveActions", { enumerable: true, get: function () { return registry_js_2.effectiveActions; } });
|
|
83
92
|
var record_engine_js_1 = require("./pipe/record-engine.js");
|
|
84
93
|
Object.defineProperty(exports, "RecordEngineImpl", { enumerable: true, get: function () { return record_engine_js_1.RecordEngineImpl; } });
|
|
85
94
|
var records_service_js_1 = require("./pipe/records-service.js");
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* Registrations are code-registered tables loaded at boot.
|
|
19
19
|
*/
|
|
20
20
|
import { type DynamicModule } from '@nestjs/common';
|
|
21
|
+
import { type FrameworkSchemas } from '@astram/core';
|
|
21
22
|
import type { QueryFn } from '../ports.js';
|
|
22
23
|
import { RECORD_ENGINE } from './tokens.js';
|
|
23
24
|
import { type EngineDbConfig } from '../adapters/mariadb-unit-of-work.js';
|
|
@@ -56,6 +57,13 @@ export interface EngineModuleOptions {
|
|
|
56
57
|
* feature module; anything passed here is merged as a base contribution. */
|
|
57
58
|
readonly registrations?: readonly ObjectTypeInput[];
|
|
58
59
|
readonly audit?: AuditSinkConfig;
|
|
60
|
+
/**
|
|
61
|
+
* OPTIONAL resolved framework database names (configurable-framework-db, ADR-0037).
|
|
62
|
+
* `FRAMEWORK_SCHEMA` tokens in registrations resolve to these, and the default audit sink
|
|
63
|
+
* writes to `schemas.audit`. Absent ⇒ resolved once from `SHIELD_DB_NAMESPACE` when
|
|
64
|
+
* `forRoot` is called (unset ⇒ today's names; invalid ⇒ throws, fail closed).
|
|
65
|
+
*/
|
|
66
|
+
readonly schemas?: FrameworkSchemas;
|
|
59
67
|
/** Test seam: a deterministic clock. */
|
|
60
68
|
readonly nowMs?: () => number;
|
|
61
69
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.module.d.ts","sourceRoot":"","sources":["../../src/module/engine.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAEL,KAAK,aAAa,EAKnB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"engine.module.d.ts","sourceRoot":"","sources":["../../src/module/engine.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAEL,KAAK,aAAa,EAKnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAuC,OAAO,EAAE,MAAM,aAAa,CAAC;AAIhF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAC7F,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,0CAA0C,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,CAAC;AAMpD;;;;2EAI2E;AAC3E,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB;;;;qCAIqC;AACrC,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;+CAI+C;AAC/C,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAmC7E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B;iFAC6E;IAC7E,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;IACpC,wCAAwC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,CACzB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CACnB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3D;AAYD,qBACa,YAAY;IACvB;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CACf,aAAa,EAAE,SAAS,eAAe,EAAE,EACzC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,SAAS,CAAC,gBAAgB,GAAG,QAAQ,CAAC,EAAE,CAAA;KAAE,GAClE,aAAa;IAMhB;2FACuF;IACvF,MAAM,CAAC,aAAa,IAAI,IAAI;IAK5B,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,aAAa;CAqG5D"}
|
|
@@ -136,12 +136,15 @@ let EngineModule = EngineModule_1 = class EngineModule {
|
|
|
136
136
|
...featureRegistrations.flat(),
|
|
137
137
|
...(options.registrations ?? []),
|
|
138
138
|
].map(toObjectType);
|
|
139
|
-
// The
|
|
140
|
-
//
|
|
139
|
+
// The framework database names, resolved ONCE here (boot) and captured: registration
|
|
140
|
+
// tokens and the default audit sink use them; nothing reads env per request (Shield #12).
|
|
141
|
+
const schemas = options.schemas ?? (0, core_1.resolveFrameworkSchemas)(process.env);
|
|
142
|
+
// The registry is built from the merged set — same register() per registration (just from
|
|
143
|
+
// the collected set, not options only); `FRAMEWORK_SCHEMA` tokens resolve at register.
|
|
141
144
|
const registryProvider = {
|
|
142
145
|
provide: ENGINE_REGISTRY,
|
|
143
146
|
useFactory: () => {
|
|
144
|
-
const registry = new registry_js_1.TableRegistry();
|
|
147
|
+
const registry = new registry_js_1.TableRegistry(schemas);
|
|
145
148
|
for (const reg of allRegistrations)
|
|
146
149
|
registry.register(reg);
|
|
147
150
|
return registry;
|
|
@@ -169,7 +172,7 @@ let EngineModule = EngineModule_1 = class EngineModule {
|
|
|
169
172
|
};
|
|
170
173
|
const auditProvider = {
|
|
171
174
|
provide: ENGINE_AUDIT,
|
|
172
|
-
useFactory: () => new mariadb_audit_sink_js_1.MariaDbAuditSink(options.audit
|
|
175
|
+
useFactory: () => new mariadb_audit_sink_js_1.MariaDbAuditSink({ schemas, ...options.audit }),
|
|
173
176
|
};
|
|
174
177
|
const engineProvider = {
|
|
175
178
|
provide: tokens_js_1.RECORD_ENGINE,
|
package/dist/pipe/context.d.ts
CHANGED
|
@@ -54,6 +54,14 @@ export interface WriteContext extends PipeBase {
|
|
|
54
54
|
* falls back to the actor's tenant. NEVER set for create.
|
|
55
55
|
*/
|
|
56
56
|
recordTenantId?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Set only for a row of a `createMany()` call: the call's one `batchId` and this row's 0-based
|
|
59
|
+
* index. Changes nothing but the audit attributes; single-row writes never carry it.
|
|
60
|
+
*/
|
|
61
|
+
batch?: {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly index: number;
|
|
64
|
+
};
|
|
57
65
|
}
|
|
58
66
|
/** A stage: a small, ordered, independently-testable unit of the pipe. */
|
|
59
67
|
export interface Stage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/pipe/context.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,gDAAgD;AAChD,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErD,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kCAAkC;AAClC,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC5C,+CAA+C;IAC/C,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC7C,oEAAoE;IACpE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/pipe/context.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,gDAAgD;AAChD,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErD,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC;IAClC,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kCAAkC;AAClC,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC5C,+CAA+C;IAC/C,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC7C,oEAAoE;IACpE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzD;AAED,0EAA0E;AAC1E,MAAM,WAAW,KAAK;IACpB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAO5F"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure SQL-fragment helpers for list reads (list-query-operators, SPEC v2 §5.B).
|
|
3
|
+
*
|
|
4
|
+
* `resolveColumn` is the ONE whitelisted field-key → column lookup the list read uses for
|
|
5
|
+
* filter, search and sort keys (later group and measure keys). The column is always the type's
|
|
6
|
+
* own registered column; an unknown key throws before any SQL is built.
|
|
7
|
+
*/
|
|
8
|
+
import type { AggregateFn, AggregateMeasure, FieldSpec, FilterOperator, ObjectType, WhereClause } from '../types.js';
|
|
9
|
+
/** What a list key is used for; names the key in the unknown-field error. */
|
|
10
|
+
export type ColumnUse = 'filter' | 'search' | 'sort' | 'groupBy' | 'measure';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a registered field key to its field and physical column, or throw
|
|
13
|
+
* `RegistrationError("list '<typeKey>': unknown <what> field '<key>'")`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveColumn(reg: ObjectType, fieldKey: string, typeKey: string, what: ColumnUse): {
|
|
16
|
+
field: FieldSpec;
|
|
17
|
+
column: string;
|
|
18
|
+
};
|
|
19
|
+
/** The `where` operators (SPEC v2 §10), exactly these keys. */
|
|
20
|
+
export declare const FILTER_OPERATORS: ReadonlySet<FilterOperator>;
|
|
21
|
+
/** Maximum number of values in one `in` operand (SPEC v2 §10). */
|
|
22
|
+
export declare const IN_CAP = 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Build the `where` clauses of a list read (SPEC v2 §5.A). Pure and deterministic: one clause
|
|
25
|
+
* per entry in object key order, ANDed by the caller.
|
|
26
|
+
*
|
|
27
|
+
* - Identifiers come only from `resolveColumn` (registered field → its column); unknown key ⇒
|
|
28
|
+
* `RegistrationError`.
|
|
29
|
+
* - Every operand passes through `coerce` (a bad one ⇒ `ValidationError(fieldKey)`) and is a
|
|
30
|
+
* `?` param; no value is ever interpolated.
|
|
31
|
+
* - A scalar is equality (`null` ⇒ `IS NULL`); an object holds exactly one §10 operator.
|
|
32
|
+
* Anything malformed or not allowed for the field type throws `RegistrationError`; it never
|
|
33
|
+
* degrades to "no filter" (Shield #1).
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildFilterClauses(reg: ObjectType, where: WhereClause | undefined, typeKey: string): {
|
|
36
|
+
clauses: string[];
|
|
37
|
+
params: unknown[];
|
|
38
|
+
};
|
|
39
|
+
/** Aggregate caps (SPEC v2 §10). */
|
|
40
|
+
export declare const GROUP_BY_CAP = 3;
|
|
41
|
+
export declare const MEASURE_CAP = 10;
|
|
42
|
+
/** How the engine converts one measure's driver value (SPEC v2 §11). */
|
|
43
|
+
export interface AggregateColumn {
|
|
44
|
+
readonly alias: string;
|
|
45
|
+
/** `count`/`sum`/`avg`, or `min`/`max` on a `number` field: BigInt/DECIMAL ⇒ `number`. */
|
|
46
|
+
readonly numeric: boolean;
|
|
47
|
+
readonly fn: AggregateFn;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the SELECT list, GROUP BY and ORDER BY of an aggregate (SPEC v2 §5.C). Pure.
|
|
51
|
+
*
|
|
52
|
+
* - `select`: group columns first as `` `col` AS `k_<n>` ``, then each measure
|
|
53
|
+
* `` FN(`col`|*) AS `alias` ``. `groupBy`/`orderBy` are the group columns in order, WITHOUT the
|
|
54
|
+
* keyword (empty strings when ungrouped). `keyMap` maps `k_<n>` back to the group field key.
|
|
55
|
+
* - Identifiers come only from `resolveColumn` (registered field → its column) or from an alias
|
|
56
|
+
* that passed `ALIAS_RE` (no backquote possible) before it is backquoted. Anything malformed
|
|
57
|
+
* throws `RegistrationError` before any SQL is built.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildAggregateSelect(reg: ObjectType, groupBy: readonly string[] | undefined, measures: readonly AggregateMeasure[], typeKey: string): {
|
|
60
|
+
select: string;
|
|
61
|
+
groupBy: string;
|
|
62
|
+
orderBy: string;
|
|
63
|
+
keyMap: Record<string, string>;
|
|
64
|
+
columns: readonly AggregateColumn[];
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=filter-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-sql.d.ts","sourceRoot":"","sources":["../../src/pipe/filter-sql.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,SAAS,EAET,cAAc,EACd,UAAU,EACV,WAAW,EACZ,MAAM,aAAa,CAAC;AAErB,6EAA6E;AAC7E,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7E;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,GACd;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAMtC;AAED,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,cAAc,CAUvD,CAAC;AAEH,kEAAkE;AAClE,eAAO,MAAM,MAAM,OAAO,CAAC;AAmB3B;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,WAAW,GAAG,SAAS,EAC9B,OAAO,EAAE,MAAM,GACd;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAwF1C;AAED,oCAAoC;AACpC,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,WAAW,KAAK,CAAC;AAgB9B,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EACtC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EACrC,OAAO,EAAE,MAAM,GACd;IACD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CACrC,CA4EA"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure SQL-fragment helpers for list reads (list-query-operators, SPEC v2 §5.B).
|
|
4
|
+
*
|
|
5
|
+
* `resolveColumn` is the ONE whitelisted field-key → column lookup the list read uses for
|
|
6
|
+
* filter, search and sort keys (later group and measure keys). The column is always the type's
|
|
7
|
+
* own registered column; an unknown key throws before any SQL is built.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MEASURE_CAP = exports.GROUP_BY_CAP = exports.IN_CAP = exports.FILTER_OPERATORS = void 0;
|
|
11
|
+
exports.resolveColumn = resolveColumn;
|
|
12
|
+
exports.buildFilterClauses = buildFilterClauses;
|
|
13
|
+
exports.buildAggregateSelect = buildAggregateSelect;
|
|
14
|
+
const errors_js_1 = require("../errors.js");
|
|
15
|
+
const registry_js_1 = require("../registry.js");
|
|
16
|
+
const validate_js_1 = require("./stages/validate.js");
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a registered field key to its field and physical column, or throw
|
|
19
|
+
* `RegistrationError("list '<typeKey>': unknown <what> field '<key>'")`.
|
|
20
|
+
*/
|
|
21
|
+
function resolveColumn(reg, fieldKey, typeKey, what) {
|
|
22
|
+
const field = reg.fields.find((f) => f.key === fieldKey);
|
|
23
|
+
if (!field) {
|
|
24
|
+
throw new errors_js_1.RegistrationError(`list '${typeKey}': unknown ${what} field '${fieldKey}'`);
|
|
25
|
+
}
|
|
26
|
+
return { field, column: (0, registry_js_1.columnOf)(field) };
|
|
27
|
+
}
|
|
28
|
+
/** The `where` operators (SPEC v2 §10), exactly these keys. */
|
|
29
|
+
exports.FILTER_OPERATORS = new Set([
|
|
30
|
+
'eq',
|
|
31
|
+
'in',
|
|
32
|
+
'gt',
|
|
33
|
+
'gte',
|
|
34
|
+
'lt',
|
|
35
|
+
'lte',
|
|
36
|
+
'between',
|
|
37
|
+
'isNull',
|
|
38
|
+
'isNotNull',
|
|
39
|
+
]);
|
|
40
|
+
/** Maximum number of values in one `in` operand (SPEC v2 §10). */
|
|
41
|
+
exports.IN_CAP = 1000;
|
|
42
|
+
/** Field types the range operators (`gt|gte|lt|lte|between`) accept (SPEC v2 §10). */
|
|
43
|
+
const RANGE_TYPES = new Set([
|
|
44
|
+
'string',
|
|
45
|
+
'number',
|
|
46
|
+
'uuid',
|
|
47
|
+
'timestamp',
|
|
48
|
+
]);
|
|
49
|
+
const RANGE_SQL = { gt: '>', gte: '>=', lt: '<', lte: '<=' };
|
|
50
|
+
/** A plain operator object: not `null`, not a `Date`, not an array. */
|
|
51
|
+
function isOperatorObject(value) {
|
|
52
|
+
return (typeof value === 'object' && value !== null && !(value instanceof Date) && !Array.isArray(value));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the `where` clauses of a list read (SPEC v2 §5.A). Pure and deterministic: one clause
|
|
56
|
+
* per entry in object key order, ANDed by the caller.
|
|
57
|
+
*
|
|
58
|
+
* - Identifiers come only from `resolveColumn` (registered field → its column); unknown key ⇒
|
|
59
|
+
* `RegistrationError`.
|
|
60
|
+
* - Every operand passes through `coerce` (a bad one ⇒ `ValidationError(fieldKey)`) and is a
|
|
61
|
+
* `?` param; no value is ever interpolated.
|
|
62
|
+
* - A scalar is equality (`null` ⇒ `IS NULL`); an object holds exactly one §10 operator.
|
|
63
|
+
* Anything malformed or not allowed for the field type throws `RegistrationError`; it never
|
|
64
|
+
* degrades to "no filter" (Shield #1).
|
|
65
|
+
*/
|
|
66
|
+
function buildFilterClauses(reg, where, typeKey) {
|
|
67
|
+
const clauses = [];
|
|
68
|
+
const params = [];
|
|
69
|
+
for (const [fieldKey, value] of Object.entries(where ?? {})) {
|
|
70
|
+
const { field, column } = resolveColumn(reg, fieldKey, typeKey, 'filter');
|
|
71
|
+
const col = `\`${column}\``;
|
|
72
|
+
const reject = (detail) => {
|
|
73
|
+
throw new errors_js_1.RegistrationError(`list '${typeKey}': filter on '${fieldKey}': ${detail}`);
|
|
74
|
+
};
|
|
75
|
+
// Scalar form: `null` ⇒ IS NULL (business rule §11); otherwise equality.
|
|
76
|
+
if (value === null) {
|
|
77
|
+
clauses.push(`${col} IS NULL`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
reject('an array value needs the `in` operator');
|
|
82
|
+
if (!isOperatorObject(value)) {
|
|
83
|
+
if (field.type === 'json')
|
|
84
|
+
reject("operator 'eq' is not allowed on a json field");
|
|
85
|
+
clauses.push(`${col} = ?`);
|
|
86
|
+
params.push((0, validate_js_1.coerce)(field, value));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Operator form: exactly one known operator key.
|
|
90
|
+
const opObject = value;
|
|
91
|
+
const keys = Object.keys(opObject);
|
|
92
|
+
if (keys.length !== 1)
|
|
93
|
+
reject(`expected exactly one operator, got ${keys.length}`);
|
|
94
|
+
const op = keys[0];
|
|
95
|
+
if (!exports.FILTER_OPERATORS.has(op))
|
|
96
|
+
reject(`unknown operator '${keys[0]}'`);
|
|
97
|
+
const operand = opObject[op];
|
|
98
|
+
switch (op) {
|
|
99
|
+
case 'isNull':
|
|
100
|
+
case 'isNotNull':
|
|
101
|
+
if (operand !== true)
|
|
102
|
+
reject(`operator '${op}' only accepts true`);
|
|
103
|
+
clauses.push(op === 'isNull' ? `${col} IS NULL` : `${col} IS NOT NULL`);
|
|
104
|
+
break;
|
|
105
|
+
case 'eq':
|
|
106
|
+
if (field.type === 'json')
|
|
107
|
+
reject("operator 'eq' is not allowed on a json field");
|
|
108
|
+
clauses.push(`${col} = ?`);
|
|
109
|
+
params.push((0, validate_js_1.coerce)(field, operand));
|
|
110
|
+
break;
|
|
111
|
+
case 'in': {
|
|
112
|
+
if (field.type === 'json')
|
|
113
|
+
reject("operator 'in' is not allowed on a json field");
|
|
114
|
+
if (!Array.isArray(operand))
|
|
115
|
+
reject("operator 'in' needs an array");
|
|
116
|
+
const values = operand;
|
|
117
|
+
if (values.length > exports.IN_CAP)
|
|
118
|
+
reject(`operator 'in' accepts at most ${exports.IN_CAP} values`);
|
|
119
|
+
if (values.length === 0) {
|
|
120
|
+
clauses.push('FALSE');
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const coerced = values.map((v) => (0, validate_js_1.coerce)(field, v));
|
|
124
|
+
clauses.push(`${col} IN (${coerced.map(() => '?').join(', ')})`);
|
|
125
|
+
params.push(...coerced);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'gt':
|
|
129
|
+
case 'gte':
|
|
130
|
+
case 'lt':
|
|
131
|
+
case 'lte':
|
|
132
|
+
if (!RANGE_TYPES.has(field.type)) {
|
|
133
|
+
reject(`operator '${op}' is not allowed on a ${field.type} field`);
|
|
134
|
+
}
|
|
135
|
+
clauses.push(`${col} ${RANGE_SQL[op]} ?`);
|
|
136
|
+
params.push((0, validate_js_1.coerce)(field, operand));
|
|
137
|
+
break;
|
|
138
|
+
case 'between': {
|
|
139
|
+
if (!RANGE_TYPES.has(field.type)) {
|
|
140
|
+
reject(`operator 'between' is not allowed on a ${field.type} field`);
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(operand) || operand.length !== 2) {
|
|
143
|
+
reject("operator 'between' needs a 2-element array");
|
|
144
|
+
}
|
|
145
|
+
const [lo, hi] = operand;
|
|
146
|
+
const coercedLo = (0, validate_js_1.coerce)(field, lo);
|
|
147
|
+
const coercedHi = (0, validate_js_1.coerce)(field, hi);
|
|
148
|
+
clauses.push(`${col} BETWEEN ? AND ?`);
|
|
149
|
+
params.push(coercedLo, coercedHi);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
// Unreachable (FILTER_OPERATORS checked above); fail closed regardless.
|
|
154
|
+
reject(`unknown operator '${String(op)}'`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { clauses, params };
|
|
158
|
+
}
|
|
159
|
+
/** Aggregate caps (SPEC v2 §10). */
|
|
160
|
+
exports.GROUP_BY_CAP = 3;
|
|
161
|
+
exports.MEASURE_CAP = 10;
|
|
162
|
+
/** A measure alias: lower-case identifier, ≤ 64 chars (SPEC v2 §10). */
|
|
163
|
+
const ALIAS_RE = /^[a-z_][a-z0-9_]{0,63}$/;
|
|
164
|
+
const AGGREGATE_SQL = {
|
|
165
|
+
count: 'COUNT',
|
|
166
|
+
sum: 'SUM',
|
|
167
|
+
avg: 'AVG',
|
|
168
|
+
min: 'MIN',
|
|
169
|
+
max: 'MAX',
|
|
170
|
+
};
|
|
171
|
+
/** Field types `min`/`max` accept (SPEC v2 §10); `sum`/`avg` accept `number` only. */
|
|
172
|
+
const MIN_MAX_TYPES = RANGE_TYPES;
|
|
173
|
+
/**
|
|
174
|
+
* Build the SELECT list, GROUP BY and ORDER BY of an aggregate (SPEC v2 §5.C). Pure.
|
|
175
|
+
*
|
|
176
|
+
* - `select`: group columns first as `` `col` AS `k_<n>` ``, then each measure
|
|
177
|
+
* `` FN(`col`|*) AS `alias` ``. `groupBy`/`orderBy` are the group columns in order, WITHOUT the
|
|
178
|
+
* keyword (empty strings when ungrouped). `keyMap` maps `k_<n>` back to the group field key.
|
|
179
|
+
* - Identifiers come only from `resolveColumn` (registered field → its column) or from an alias
|
|
180
|
+
* that passed `ALIAS_RE` (no backquote possible) before it is backquoted. Anything malformed
|
|
181
|
+
* throws `RegistrationError` before any SQL is built.
|
|
182
|
+
*/
|
|
183
|
+
function buildAggregateSelect(reg, groupBy, measures, typeKey) {
|
|
184
|
+
const reject = (detail) => {
|
|
185
|
+
throw new errors_js_1.RegistrationError(`aggregate '${typeKey}': ${detail}`);
|
|
186
|
+
};
|
|
187
|
+
const groups = groupBy ?? [];
|
|
188
|
+
if (!Array.isArray(groups))
|
|
189
|
+
reject('groupBy must be an array of field keys');
|
|
190
|
+
if (groups.length > exports.GROUP_BY_CAP)
|
|
191
|
+
reject(`groupBy accepts at most ${exports.GROUP_BY_CAP} keys`);
|
|
192
|
+
const groupCols = [];
|
|
193
|
+
const selectParts = [];
|
|
194
|
+
const keyMap = {};
|
|
195
|
+
groups.forEach((key, i) => {
|
|
196
|
+
if (groups.indexOf(key) !== i)
|
|
197
|
+
reject(`duplicate groupBy key '${key}'`);
|
|
198
|
+
const { field, column } = resolveColumn(reg, key, typeKey, 'groupBy');
|
|
199
|
+
if (field.type === 'json')
|
|
200
|
+
reject(`groupBy on json field '${key}' is not allowed`);
|
|
201
|
+
const alias = `k_${i}`;
|
|
202
|
+
groupCols.push(`\`${column}\``);
|
|
203
|
+
selectParts.push(`\`${column}\` AS \`${alias}\``);
|
|
204
|
+
keyMap[alias] = key;
|
|
205
|
+
});
|
|
206
|
+
if (!Array.isArray(measures) || measures.length === 0)
|
|
207
|
+
reject('at least one measure is required');
|
|
208
|
+
if (measures.length > exports.MEASURE_CAP)
|
|
209
|
+
reject(`at most ${exports.MEASURE_CAP} measures are allowed`);
|
|
210
|
+
const aliases = new Set();
|
|
211
|
+
const columns = [];
|
|
212
|
+
for (const raw of measures) {
|
|
213
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
214
|
+
reject('each measure must be an object');
|
|
215
|
+
}
|
|
216
|
+
const m = raw;
|
|
217
|
+
const alias = m.as;
|
|
218
|
+
if (typeof alias !== 'string' || !ALIAS_RE.test(alias)) {
|
|
219
|
+
reject(`measure alias '${String(alias)}' must match ${String(ALIAS_RE)}`);
|
|
220
|
+
}
|
|
221
|
+
// `__proto__` matches ALIAS_RE but is the one name that, assigned on a plain object, would set
|
|
222
|
+
// the prototype of the shaped result instead of creating a key: reject it outright.
|
|
223
|
+
if (alias === '__proto__')
|
|
224
|
+
reject(`measure alias '${alias}' is reserved`);
|
|
225
|
+
if (aliases.has(alias))
|
|
226
|
+
reject(`duplicate measure alias '${alias}'`);
|
|
227
|
+
// A measure alias may not shadow a group key or a generated `k_<n>` group column. Own-property
|
|
228
|
+
// check only: `in` walks the prototype chain and would reject `constructor`, `toString`, ….
|
|
229
|
+
if (groups.includes(alias) || Object.prototype.hasOwnProperty.call(keyMap, alias)) {
|
|
230
|
+
reject(`measure alias '${alias}' collides with a groupBy key`);
|
|
231
|
+
}
|
|
232
|
+
aliases.add(alias);
|
|
233
|
+
const fn = m.fn;
|
|
234
|
+
if (!Object.prototype.hasOwnProperty.call(AGGREGATE_SQL, fn)) {
|
|
235
|
+
reject(`unknown aggregate function '${String(fn)}'`);
|
|
236
|
+
}
|
|
237
|
+
let arg = '*';
|
|
238
|
+
let numeric = fn === 'count' || fn === 'sum' || fn === 'avg';
|
|
239
|
+
if (m.field === undefined) {
|
|
240
|
+
if (fn !== 'count')
|
|
241
|
+
reject(`'${fn}' needs a field`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const { field, column } = resolveColumn(reg, m.field, typeKey, 'measure');
|
|
245
|
+
if ((fn === 'sum' || fn === 'avg') && field.type !== 'number') {
|
|
246
|
+
reject(`'${fn}' is not allowed on ${field.type} field '${m.field}'`);
|
|
247
|
+
}
|
|
248
|
+
if ((fn === 'min' || fn === 'max') && !MIN_MAX_TYPES.has(field.type)) {
|
|
249
|
+
reject(`'${fn}' is not allowed on ${field.type} field '${m.field}'`);
|
|
250
|
+
}
|
|
251
|
+
if (field.type === 'number')
|
|
252
|
+
numeric = true;
|
|
253
|
+
arg = `\`${column}\``;
|
|
254
|
+
}
|
|
255
|
+
selectParts.push(`${AGGREGATE_SQL[fn]}(${arg}) AS \`${alias}\``);
|
|
256
|
+
columns.push({ alias, numeric, fn });
|
|
257
|
+
}
|
|
258
|
+
const groupList = groupCols.join(', ');
|
|
259
|
+
return {
|
|
260
|
+
select: selectParts.join(', '),
|
|
261
|
+
groupBy: groupList,
|
|
262
|
+
orderBy: groupCols.map((c) => `${c} ASC`).join(', '),
|
|
263
|
+
keyMap,
|
|
264
|
+
columns,
|
|
265
|
+
};
|
|
266
|
+
}
|