@astram/engine 0.5.3 → 0.7.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 (37) hide show
  1. package/dist/adapters/mariadb-unit-of-work.d.ts +8 -0
  2. package/dist/adapters/mariadb-unit-of-work.d.ts.map +1 -1
  3. package/dist/adapters/mariadb-unit-of-work.js +1 -0
  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 +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -1
  10. package/dist/pipe/context.d.ts +8 -0
  11. package/dist/pipe/context.d.ts.map +1 -1
  12. package/dist/pipe/filter-sql.d.ts +66 -0
  13. package/dist/pipe/filter-sql.d.ts.map +1 -0
  14. package/dist/pipe/filter-sql.js +266 -0
  15. package/dist/pipe/record-engine.d.ts +34 -1
  16. package/dist/pipe/record-engine.d.ts.map +1 -1
  17. package/dist/pipe/record-engine.js +256 -87
  18. package/dist/pipe/records-service.d.ts +19 -1
  19. package/dist/pipe/records-service.d.ts.map +1 -1
  20. package/dist/pipe/records-service.js +32 -0
  21. package/dist/pipe/stages/audit.d.ts.map +1 -1
  22. package/dist/pipe/stages/audit.js +8 -1
  23. package/dist/pipe/stages/fetch-record.d.ts +11 -1
  24. package/dist/pipe/stages/fetch-record.d.ts.map +1 -1
  25. package/dist/pipe/stages/fetch-record.js +18 -9
  26. package/dist/pipe/stages/permission-gate.d.ts +8 -0
  27. package/dist/pipe/stages/permission-gate.d.ts.map +1 -1
  28. package/dist/pipe/stages/permission-gate.js +15 -9
  29. package/dist/pipe/stages/validate.d.ts +6 -0
  30. package/dist/pipe/stages/validate.d.ts.map +1 -1
  31. package/dist/pipe/stages/validate.js +22 -8
  32. package/dist/registry.d.ts +13 -0
  33. package/dist/registry.d.ts.map +1 -1
  34. package/dist/registry.js +66 -0
  35. package/dist/types.d.ts +112 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +2 -2
@@ -17,6 +17,14 @@ export interface EngineDbConfig {
17
17
  readonly database: string;
18
18
  /** ADR-0018 budgeted connection cap for this pool. */
19
19
  readonly connectionLimit: number;
20
+ /**
21
+ * The driver's TLS option (Phase 16): built by the composition root from `APP_DB_TLS` with
22
+ * guard's `mariadbSslOption` (the engine never reads env or imports guard). Absent ⇒ no TLS.
23
+ */
24
+ readonly ssl?: {
25
+ readonly ca?: string;
26
+ readonly rejectUnauthorized: boolean;
27
+ };
20
28
  }
21
29
  export declare class MariaDbUnitOfWork implements UnitOfWork {
22
30
  private readonly pool;
@@ -1 +1 @@
1
- {"version":3,"file":"mariadb-unit-of-work.d.ts","sourceRoot":"","sources":["../../src/adapters/mariadb-unit-of-work.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,qBAAa,iBAAkB,YAAW,UAAU;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;gBAEhB,MAAM,EAAE,cAAc;IAc5B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
1
+ {"version":3,"file":"mariadb-unit-of-work.d.ts","sourceRoot":"","sources":["../../src/adapters/mariadb-unit-of-work.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/E;AAED,qBAAa,iBAAkB,YAAW,UAAU;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;gBAEhB,MAAM,EAAE,cAAc;IAe5B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAqBxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -27,6 +27,7 @@ class MariaDbUnitOfWork {
27
27
  connectTimeout: 10_000,
28
28
  timezone: 'Z',
29
29
  multipleStatements: false,
30
+ ...(config.ssl ? { ssl: config.ssl } : {}),
30
31
  });
31
32
  }
32
33
  async run(body) {
@@ -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,14 @@
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
22
  export { canonicalUuid } from './uuid.js';
23
23
  export { EngineError, CapabilityNotSupportedError, RegistrationError, AuthorizationDeniedError, ValidationError, EngineUnavailableError, } from './errors.js';
24
24
  export { ObjectType, Field, PrimaryKey, reflectObjectType, type ObjectTypeOptions, type FieldOptions, type PrimaryKeyOptions, } from './decorators/object-type.decorator.js';
25
25
  export { AccessBundle, reflectAccessBundle, type AccessBundleOptions, type AccessBundlePermission, type AccessBundleSpec, } from './decorators/access-bundle.decorator.js';
26
26
  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';
27
+ export { TableRegistry, columnOf, uuidPkColumn, isOwned, ownerColumn, tenantColumnOf, columnForRole, requireRole, effectiveActions, } from './registry.js';
28
28
  export { RecordEngineImpl, type RecordEngineDeps } from './pipe/record-engine.js';
29
29
  export { RecordsService, BoundRecords, type ServiceAccountResolver, SERVICE_ACCOUNT_RESOLVER, } from './pipe/records-service.js';
30
30
  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;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,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,7 +19,7 @@
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.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).
@@ -80,6 +80,7 @@ Object.defineProperty(exports, "ownerColumn", { enumerable: true, get: function
80
80
  Object.defineProperty(exports, "tenantColumnOf", { enumerable: true, get: function () { return registry_js_1.tenantColumnOf; } });
81
81
  Object.defineProperty(exports, "columnForRole", { enumerable: true, get: function () { return registry_js_1.columnForRole; } });
82
82
  Object.defineProperty(exports, "requireRole", { enumerable: true, get: function () { return registry_js_1.requireRole; } });
83
+ Object.defineProperty(exports, "effectiveActions", { enumerable: true, get: function () { return registry_js_1.effectiveActions; } });
83
84
  var record_engine_js_1 = require("./pipe/record-engine.js");
84
85
  Object.defineProperty(exports, "RecordEngineImpl", { enumerable: true, get: function () { return record_engine_js_1.RecordEngineImpl; } });
85
86
  var records_service_js_1 = require("./pipe/records-service.js");
@@ -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
+ }
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import type { TableRegistry } from '../registry.js';
19
19
  import type { AuditPort, DecisionPort, UnitOfWork, QueryFn } from '../ports.js';
20
- import type { ListQuery, Principal, RecordEngine, RecordId, RecordInput, RecordPage, RecordView, ObjectType } from '../types.js';
20
+ import type { AggregateQuery, AggregateResult, ListQuery, Principal, RecordEngine, RecordId, RecordInput, RecordPage, RecordView, ObjectType } from '../types.js';
21
21
  export interface RecordEngineDeps {
22
22
  readonly registry: TableRegistry;
23
23
  readonly uow: UnitOfWork;
@@ -98,6 +98,14 @@ export declare class RecordEngineImpl implements RecordEngine {
98
98
  */
99
99
  lockForUpdate(typeKey: string, id: RecordId, actor: Principal): Promise<void>;
100
100
  private lockIn;
101
+ /** Record-level decision for one declared action, no write (see `RecordEngine.authorize`). */
102
+ authorize(typeKey: string, id: RecordId, action: string, actor: Principal): Promise<void>;
103
+ /**
104
+ * The write path's gates without the write (SPEC §5 E): declared action → type pre-gate (no
105
+ * SQL) → `whereById` fetch → record-level decide on the row's facts and tenant. Read-only unit,
106
+ * so a deny never makes an open transaction rollback-only. No audit, no list scope.
107
+ */
108
+ private authorizeIn;
101
109
  /**
102
110
  * Run one op's body in the right unit of work: the bound/ambient transaction when there is
103
111
  * one, else a fresh `uow.run` (the original per-op transaction). A WRITE failure inside a
@@ -108,7 +116,16 @@ export declare class RecordEngineImpl implements RecordEngine {
108
116
  create(typeKey: string, input: RecordInput, actor: Principal): Promise<RecordId>;
109
117
  update(typeKey: string, id: RecordId, input: RecordInput, actor: Principal): Promise<void>;
110
118
  delete(typeKey: string, id: RecordId, actor: Principal): Promise<void>;
119
+ createMany(typeKey: string, inputs: readonly RecordInput[], actor: Principal): Promise<RecordId[]>;
111
120
  private createIn;
121
+ /**
122
+ * Bulk = single (Shield #7, SPEC v2 §5.G): every row runs the unchanged single-row create pipe,
123
+ * in order, in ONE unit of work (joins an outer transaction). No batched INSERT, no decision
124
+ * shared across rows. The first failing row throws and rollback-only state discards the whole
125
+ * batch, its audit rows included. Inputs are checked before the type lookup and before any
126
+ * transaction opens, so a bad call neither touches the DB nor poisons an outer transaction.
127
+ */
128
+ private createManyIn;
112
129
  private updateIn;
113
130
  private deleteIn;
114
131
  private gateRead;
@@ -118,7 +135,23 @@ export declare class RecordEngineImpl implements RecordEngine {
118
135
  get(typeKey: string, id: RecordId, actor: Principal): Promise<RecordView | null>;
119
136
  list(typeKey: string, query: ListQuery, actor: Principal): Promise<RecordPage>;
120
137
  private getIn;
138
+ /**
139
+ * The ONE read WHERE of a list (SPEC v2 §5.D): list-scope fragment resolved first, tenant
140
+ * clause, readScope wall, equality `where`, search, list-scope fragment. Shared by the data
141
+ * SELECT and the COUNT (and later `aggregate`), so they cannot diverge. Async because it
142
+ * awaits the list-scope seam; takes no `q` (no DB read of its own). Callers run `gateRead`
143
+ * first.
144
+ */
145
+ private buildReadWhere;
121
146
  private listIn;
147
+ aggregate(typeKey: string, query: AggregateQuery, actor: Principal): Promise<AggregateResult>;
148
+ /**
149
+ * Aggregate over exactly the rows `list()` returns (SPEC v2 §5.F, Shield #8): the SAME gate
150
+ * (object-type `read`, first) and the SAME `buildReadWhere`, then ONE
151
+ * `SELECT … FROM <table> <where> [GROUP BY … ORDER BY …] LIMIT ?`. No table alias, so a
152
+ * list-scope fragment's bare columns resolve. Read-only: no audit row, no write.
153
+ */
154
+ private aggregateIn;
122
155
  /** The physical column of a `tenantId`-trusted field, if the table has one. */
123
156
  private tenantColumn;
124
157
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EACV,UAAU,EACX,MAAM,aAAa,CAAC;AAsCrB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;;OAoBG;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;IAEnB;;;;;;;;;;;;;;OAcG;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;AAED,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAElB;IACd,OAAO,CAAC,QAAQ,CAAC,SAAS,CAMZ;IACd,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,wFAAwF;IACxF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;gBAEjD,IAAI,EAAE,gBAAgB;IA8BlC;;;;;;;OAOG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;YAIxD,aAAa;IA8B3B,uEAAuE;IACvE,OAAO,CAAC,SAAS;IAYjB;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAIrE,MAAM;IAsBpB;;;;OAIG;YACW,MAAM;YAgBN,QAAQ;IAuChB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIhF,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1F,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAI9D,QAAQ;YAWR,QAAQ;YAUR,QAAQ;YAWR,QAAQ;IAkCtB,OAAO,CAAC,SAAS;IAIjB,mEAAmE;IACnE,OAAO,CAAC,OAAO;IAiBT,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAIhF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;YAItE,KAAK;YA2EL,MAAM;IAuIpB,+EAA+E;IAC/E,OAAO,CAAC,YAAY;IAKpB;;;;;;;;OAQG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,eAAe;CAmBxB"}
1
+ {"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"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"}