@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.
Files changed (41) hide show
  1. package/dist/adapters/mariadb-audit-sink.d.ts +10 -1
  2. package/dist/adapters/mariadb-audit-sink.d.ts.map +1 -1
  3. package/dist/adapters/mariadb-audit-sink.js +8 -1
  4. package/dist/decorators/object-type.decorator.d.ts +10 -0
  5. package/dist/decorators/object-type.decorator.d.ts.map +1 -1
  6. package/dist/decorators/object-type.decorator.js +5 -0
  7. package/dist/index.d.ts +4 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +19 -10
  10. package/dist/module/engine.module.d.ts +8 -0
  11. package/dist/module/engine.module.d.ts.map +1 -1
  12. package/dist/module/engine.module.js +7 -4
  13. package/dist/pipe/context.d.ts +8 -0
  14. package/dist/pipe/context.d.ts.map +1 -1
  15. package/dist/pipe/filter-sql.d.ts +66 -0
  16. package/dist/pipe/filter-sql.d.ts.map +1 -0
  17. package/dist/pipe/filter-sql.js +266 -0
  18. package/dist/pipe/record-engine.d.ts +34 -1
  19. package/dist/pipe/record-engine.d.ts.map +1 -1
  20. package/dist/pipe/record-engine.js +256 -87
  21. package/dist/pipe/records-service.d.ts +19 -1
  22. package/dist/pipe/records-service.d.ts.map +1 -1
  23. package/dist/pipe/records-service.js +32 -0
  24. package/dist/pipe/stages/audit.d.ts.map +1 -1
  25. package/dist/pipe/stages/audit.js +8 -1
  26. package/dist/pipe/stages/fetch-record.d.ts +11 -1
  27. package/dist/pipe/stages/fetch-record.d.ts.map +1 -1
  28. package/dist/pipe/stages/fetch-record.js +18 -9
  29. package/dist/pipe/stages/permission-gate.d.ts +8 -0
  30. package/dist/pipe/stages/permission-gate.d.ts.map +1 -1
  31. package/dist/pipe/stages/permission-gate.js +15 -9
  32. package/dist/pipe/stages/validate.d.ts +6 -0
  33. package/dist/pipe/stages/validate.d.ts.map +1 -1
  34. package/dist/pipe/stages/validate.js +22 -8
  35. package/dist/registry.d.ts +38 -2
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +128 -2
  38. package/dist/types.d.ts +133 -3
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/types.js +17 -1
  41. 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 (default shield_audit). */
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,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,MAAM,GAAE,eAAoB;IAMlC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB/D"}
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 ?? 'shield_audit';
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,CA+BnE"}
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';
@@ -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;AAKpD,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,GACZ,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"}
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 registry_js_1 = require("./registry.js");
75
- Object.defineProperty(exports, "TableRegistry", { enumerable: true, get: function () { return registry_js_1.TableRegistry; } });
76
- Object.defineProperty(exports, "columnOf", { enumerable: true, get: function () { return registry_js_1.columnOf; } });
77
- Object.defineProperty(exports, "uuidPkColumn", { enumerable: true, get: function () { return registry_js_1.uuidPkColumn; } });
78
- Object.defineProperty(exports, "isOwned", { enumerable: true, get: function () { return registry_js_1.isOwned; } });
79
- Object.defineProperty(exports, "ownerColumn", { enumerable: true, get: function () { return registry_js_1.ownerColumn; } });
80
- Object.defineProperty(exports, "tenantColumnOf", { enumerable: true, get: function () { return registry_js_1.tenantColumnOf; } });
81
- Object.defineProperty(exports, "columnForRole", { enumerable: true, get: function () { return registry_js_1.columnForRole; } });
82
- Object.defineProperty(exports, "requireRole", { enumerable: true, get: function () { return registry_js_1.requireRole; } });
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;AAExB,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,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;CAiG5D"}
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 registry is built from the merged set. Behavior identical to before — same
140
- // register() per registration (just from the collected set, not options only).
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,
@@ -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;CACzB;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"}
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
+ }