@astram/engine 0.1.3 → 0.2.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/decorators/object-type.decorator.d.ts +7 -1
- package/dist/decorators/object-type.decorator.d.ts.map +1 -1
- package/dist/decorators/object-type.decorator.js +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/module/engine.module.d.ts +14 -1
- package/dist/module/engine.module.d.ts.map +1 -1
- package/dist/module/engine.module.js +1 -0
- package/dist/pipe/record-engine.d.ts +51 -1
- package/dist/pipe/record-engine.d.ts.map +1 -1
- package/dist/pipe/record-engine.js +115 -7
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/package.json +2 -2
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `undefined` keys and break `deepStrictEqual`).
|
|
25
25
|
*/
|
|
26
26
|
import 'reflect-metadata';
|
|
27
|
-
import type { AccessDeclaration, FieldType, IdentityRole, JunctionSpec, ObjectType as ObjectTypeContract, PkStrategy, TrustedSource } from '../types.js';
|
|
27
|
+
import type { AccessDeclaration, FieldType, IdentityRole, JunctionSpec, ObjectType as ObjectTypeContract, PkStrategy, ReadScope, TrustedSource } from '../types.js';
|
|
28
28
|
/**
|
|
29
29
|
* Class-level options for `@ObjectType`. Mirrors the top-level {@link ObjectType}
|
|
30
30
|
* shape EXCEPT `primaryKey` (declared separately via `@PrimaryKey`) and `fields`
|
|
@@ -45,6 +45,12 @@ export interface ObjectTypeOptions {
|
|
|
45
45
|
* absent/contradictory declaration at boot. See {@link AccessDeclaration}.
|
|
46
46
|
*/
|
|
47
47
|
readonly access: AccessDeclaration;
|
|
48
|
+
/**
|
|
49
|
+
* OPTIONAL read-scope declaration (ADR-0027) — how LIST/GET reads are walled to the
|
|
50
|
+
* caller's reachable subtree, DECOUPLED from the write path. Reflected with the SAME
|
|
51
|
+
* omit-when-absent discipline as `junctions`/`extensible`. See {@link ReadScope}.
|
|
52
|
+
*/
|
|
53
|
+
readonly readScope?: ReadScope;
|
|
48
54
|
}
|
|
49
55
|
/** Per-member options for `@Field` (the field's `key` is read from the member name). */
|
|
50
56
|
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,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;
|
|
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;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,CAQ3D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,CAIlE;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,kBAAkB,CAwCnE"}
|
|
@@ -142,5 +142,9 @@ function reflectObjectType(cls) {
|
|
|
142
142
|
// registry's `validate` (chokepoint 1) is what rejects the absence at boot.
|
|
143
143
|
if (opts.access !== undefined)
|
|
144
144
|
out.access = opts.access;
|
|
145
|
+
// `readScope` (ADR-0027) reflected AS-IS with the omit-when-absent discipline — a
|
|
146
|
+
// type without it omits the key (matching the literal), so `deepStrictEqual` holds.
|
|
147
|
+
if (opts.readScope !== undefined)
|
|
148
|
+
out.readScope = opts.readScope;
|
|
145
149
|
return out;
|
|
146
150
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* the `TableRegistry`, and the ports. Exported so the framework + integration
|
|
16
16
|
* tests can compose the engine directly; NOT for feature code.
|
|
17
17
|
*/
|
|
18
|
-
export type { RecordEngine, AccessDeclaration, FieldSpec, FieldType, TrustedSource, IdentityRole, PkStrategy, JunctionSpec, RecordInput, RecordId, RecordView, ListQuery, RecordPage, Principal, } from './types.js';
|
|
18
|
+
export type { RecordEngine, AccessDeclaration, FieldSpec, FieldType, TrustedSource, IdentityRole, PkStrategy, JunctionSpec, RecordInput, RecordId, RecordView, ListQuery, RecordPage, Principal, ReadScope, } from './types.js';
|
|
19
|
+
export { READ_SCOPE_NODES_TOKEN } from './types.js';
|
|
19
20
|
export { EngineError, CapabilityNotSupportedError, RegistrationError, AuthorizationDeniedError, ValidationError, EngineUnavailableError, } from './errors.js';
|
|
20
21
|
export { ObjectType, Field, PrimaryKey, reflectObjectType, type ObjectTypeOptions, type FieldOptions, type PrimaryKeyOptions, } from './decorators/object-type.decorator.js';
|
|
21
22
|
export { AccessBundle, reflectAccessBundle, type AccessBundleOptions, type AccessBundlePermission, type AccessBundleSpec, } from './decorators/access-bundle.decorator.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;;;;;;;;;;;;;;;;GAgBG;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,GACV,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;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,GACV,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEpD,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;AACnC,OAAO,EACL,iBAAiB,EACjB,KAAK,cAAc,GACpB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAC1F,YAAY,EACV,OAAO,EACP,UAAU,EACV,YAAY,EACZ,SAAS,GACV,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
* tests can compose the engine directly; NOT for feature code.
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.MariaDbAuditSink = exports.MariaDbUnitOfWork = 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 = void 0;
|
|
20
|
+
exports.MariaDbAuditSink = exports.MariaDbUnitOfWork = 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.READ_SCOPE_NODES_TOKEN = void 0;
|
|
21
|
+
// The read-scope subtree-id placeholder token (ADR-0027) — a `subtreeSubquery` template
|
|
22
|
+
// uses it; the engine expands it to a `?`-list. Exported so per-type declarations can
|
|
23
|
+
// reference the canonical token (rather than a stringly-typed duplicate).
|
|
24
|
+
var types_js_1 = require("./types.js");
|
|
25
|
+
Object.defineProperty(exports, "READ_SCOPE_NODES_TOKEN", { enumerable: true, get: function () { return types_js_1.READ_SCOPE_NODES_TOKEN; } });
|
|
21
26
|
var errors_js_1 = require("./errors.js");
|
|
22
27
|
Object.defineProperty(exports, "EngineError", { enumerable: true, get: function () { return errors_js_1.EngineError; } });
|
|
23
28
|
Object.defineProperty(exports, "CapabilityNotSupportedError", { enumerable: true, get: function () { return errors_js_1.CapabilityNotSupportedError; } });
|
|
@@ -22,7 +22,7 @@ import type { QueryFn } from '../ports.js';
|
|
|
22
22
|
import { RECORD_ENGINE } from './tokens.js';
|
|
23
23
|
import { type EngineDbConfig } from '../adapters/mariadb-unit-of-work.js';
|
|
24
24
|
import { type AuditSinkConfig } from '../adapters/mariadb-audit-sink.js';
|
|
25
|
-
import type { ObjectType } from '../types.js';
|
|
25
|
+
import type { ObjectType, Principal } from '../types.js';
|
|
26
26
|
import { type AccessBundleSpec } from '../decorators/access-bundle.decorator.js';
|
|
27
27
|
/**
|
|
28
28
|
* Input accepted by `forFeature` / `forRoot({registrations})`: either a plain
|
|
@@ -67,6 +67,19 @@ export interface EngineModuleOptions {
|
|
|
67
67
|
* the engine never imports guard; this is the injection seam.
|
|
68
68
|
*/
|
|
69
69
|
readonly onRecordDeleted?: (query: QueryFn, typeKey: string, recordId: string, tenantId: string) => Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* OPTIONAL record-level LIST SCOPE (design §8.3, Invariant #8). The consumer wires guard's
|
|
72
|
+
* framework-constructed `buildListScopeFn(inputs)` here — the engine's `list()` ANDs the
|
|
73
|
+
* returned SAFE parameterized WHERE fragment into BOTH the data SELECT and the COUNT (the
|
|
74
|
+
* in-query visible-set filter). ADR-0003: the engine never imports guard; this is the
|
|
75
|
+
* injection seam, exactly like `onRecordDeleted`. Absent ⇒ today's bare-tenant behavior.
|
|
76
|
+
* A throw PROPAGATES (fail-closed). It runs only AFTER the coarse object-type gate allows
|
|
77
|
+
* the read — it narrows rows within an allowed list, never substitutes for the gate.
|
|
78
|
+
*/
|
|
79
|
+
readonly listScope?: (reg: ObjectType, actor: Principal, action: string) => Promise<{
|
|
80
|
+
sql: string;
|
|
81
|
+
params: readonly unknown[];
|
|
82
|
+
}>;
|
|
70
83
|
}
|
|
71
84
|
export declare class EngineModule {
|
|
72
85
|
/**
|
|
@@ -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,MAAM,aAAa,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;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"}
|
|
@@ -181,6 +181,7 @@ let EngineModule = EngineModule_1 = class EngineModule {
|
|
|
181
181
|
audit,
|
|
182
182
|
...(options.nowMs ? { nowMs: options.nowMs } : {}),
|
|
183
183
|
...(options.onRecordDeleted ? { onRecordDeleted: options.onRecordDeleted } : {}),
|
|
184
|
+
...(options.listScope ? { listScope: options.listScope } : {}),
|
|
184
185
|
};
|
|
185
186
|
return new record_engine_js_1.RecordEngineImpl(deps);
|
|
186
187
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { TableRegistry } from '../registry.js';
|
|
17
17
|
import type { AuditPort, DecisionPort, UnitOfWork, QueryFn } from '../ports.js';
|
|
18
|
-
import type { ListQuery, Principal, RecordEngine, RecordId, RecordInput, RecordPage, RecordView } from '../types.js';
|
|
18
|
+
import type { ListQuery, Principal, RecordEngine, RecordId, RecordInput, RecordPage, RecordView, ObjectType } from '../types.js';
|
|
19
19
|
export interface RecordEngineDeps {
|
|
20
20
|
readonly registry: TableRegistry;
|
|
21
21
|
readonly uow: UnitOfWork;
|
|
@@ -45,6 +45,25 @@ export interface RecordEngineDeps {
|
|
|
45
45
|
* store error) prevents leaving the record deleted with orphaned grants (fail-closed).
|
|
46
46
|
*/
|
|
47
47
|
readonly onRecordDeleted?: (query: QueryFn, typeKey: string, recordId: string, tenantId: string) => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* OPTIONAL record-level LIST SCOPE (design §8.3, the list-scoping engine). When present,
|
|
50
|
+
* list() ANDs the returned SAFE parameterized WHERE fragment into BOTH the data SELECT and
|
|
51
|
+
* the COUNT — the in-query visible-set filter (Invariant #8). The CONSUMER wires guard's
|
|
52
|
+
* framework-constructed `buildListScopeFn` here (ADR-0003: the engine never imports guard —
|
|
53
|
+
* this is the injection seam, exactly like `onRecordDeleted`). Absent ⇒ today's bare-tenant
|
|
54
|
+
* behavior (back-compat until wired). Returns `{ sql, params }`: the fragment ANDed into the
|
|
55
|
+
* WHERE. A throw PROPAGATES (fail-closed — the list read fails, never returns unscoped rows).
|
|
56
|
+
*
|
|
57
|
+
* ⛔ SCOPE RUNS AFTER THE GATE: list() calls the coarse object-type gate (gateRead → decide())
|
|
58
|
+
* FIRST; the scope narrows rows WITHIN an already-allowed object-type list and NEVER substitutes
|
|
59
|
+
* for the gate. This ordering is load-bearing: a ROOT actor's scope is root-unbounded (no tenant
|
|
60
|
+
* clause, `(TRUE)` union) — the ONLY thing preventing root from reading every tenant's data-plane
|
|
61
|
+
* rows is that gateRead DENIES root on a data-plane type before any scope is built.
|
|
62
|
+
*/
|
|
63
|
+
readonly listScope?: (reg: ObjectType, actor: Principal, action: string) => Promise<{
|
|
64
|
+
sql: string;
|
|
65
|
+
params: readonly unknown[];
|
|
66
|
+
}>;
|
|
48
67
|
}
|
|
49
68
|
export declare class RecordEngineImpl implements RecordEngine {
|
|
50
69
|
private readonly registry;
|
|
@@ -53,6 +72,7 @@ export declare class RecordEngineImpl implements RecordEngine {
|
|
|
53
72
|
private readonly audit;
|
|
54
73
|
private readonly now;
|
|
55
74
|
private readonly onRecordDeleted;
|
|
75
|
+
private readonly listScope;
|
|
56
76
|
/** The FIXED write-stage order. Built once; identical for every write. */
|
|
57
77
|
private readonly writeStages;
|
|
58
78
|
constructor(deps: RecordEngineDeps);
|
|
@@ -68,5 +88,35 @@ export declare class RecordEngineImpl implements RecordEngine {
|
|
|
68
88
|
list(typeKey: string, query: ListQuery, actor: Principal): Promise<RecordPage>;
|
|
69
89
|
/** The physical column of a `tenantId`-trusted field, if the table has one. */
|
|
70
90
|
private tenantColumn;
|
|
91
|
+
/**
|
|
92
|
+
* The set of tenant nodes a non-root actor's reads are bounded to (ADR-0027): its
|
|
93
|
+
* active-scope subtree (active node + descendants), resolved once at login and cached on
|
|
94
|
+
* the principal (`scopeSubtreeNodeIds`). The engine's built-in tenant clause emits
|
|
95
|
+
* `tenant_col IN (these)` — the SAME set the tenant gate + list-scope use, so the three
|
|
96
|
+
* agree (no self-node collapse of the subtree). FAIL-SAFE + non-widening: a malformed or
|
|
97
|
+
* absent subtree falls back to just the actor's home node (`[actor.tenantId]`) — exactly
|
|
98
|
+
* today's flat self-tenant isolation; NEVER an empty set (which would hide all own rows).
|
|
99
|
+
*/
|
|
100
|
+
private tenantScopeNodes;
|
|
101
|
+
/**
|
|
102
|
+
* The read-scope predicate for a type's LIST/GET reads (ADR-0027, Task 4.1) — the wall
|
|
103
|
+
* that filters a `system` type's rows to the caller's reachable subtree, DECOUPLED from
|
|
104
|
+
* the write path (`readScope` NEVER stamps a write). Shared by list() AND get() so both
|
|
105
|
+
* apply the SAME wall. Returns `null` when there is nothing to AND:
|
|
106
|
+
* - the type declares no `readScope`, OR
|
|
107
|
+
* - the actor is ROOT (`isRoot === true`, strict) — root spans all, no clause.
|
|
108
|
+
* Otherwise ANDs, over the actor's subtree nodes (`tenantScopeNodes` — the SAME set the
|
|
109
|
+
* tenant clause + list-scope use, so the three agree):
|
|
110
|
+
* - `nodeColumn` → `` `<column>` IN (?, ...) `` — subtree ids `?`-bound.
|
|
111
|
+
* - `subtreeSubquery` → `` `<pkColumn>` IN (<subquerySql, :nodes → IN (?, ...)>) `` — the
|
|
112
|
+
* FIXED framework template with the subtree ids `?`-bound at `:nodes`.
|
|
113
|
+
*
|
|
114
|
+
* INJECTION-FREE BY CONSTRUCTION: the ONLY dynamic values are the `?`-bound subtree node
|
|
115
|
+
* ids; every identifier is a compile-time constant (the type's own column, or the fixed
|
|
116
|
+
* template). No node value is ever interpolated into the SQL string. FAIL-CLOSED: an
|
|
117
|
+
* empty subtree (shouldn't happen — `tenantScopeNodes` never returns empty) would still
|
|
118
|
+
* emit `IN ()`, which matches nothing (deny), never a widening clause.
|
|
119
|
+
*/
|
|
120
|
+
private readScopeClause;
|
|
71
121
|
}
|
|
72
122
|
//# sourceMappingURL=record-engine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,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,
|
|
1
|
+
{"version":3,"file":"record-engine.d.ts","sourceRoot":"","sources":["../../src/pipe/record-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,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;AAYrB,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,CAOlB;IACd,OAAO,CAAC,QAAQ,CAAC,SAAS,CAMZ;IACd,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;gBAEnC,IAAI,EAAE,gBAAgB;YAyBpB,QAAQ;IAsChB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAMhF,MAAM,CACV,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC;IAIV,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAM9D,QAAQ;IAiCtB,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;IA4EhF,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAsHpF,+EAA+E;IAC/E,OAAO,CAAC,YAAY;IAKpB;;;;;;;;OAQG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,eAAe;CAmBxB"}
|
|
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
exports.RecordEngineImpl = void 0;
|
|
19
19
|
const errors_js_1 = require("../errors.js");
|
|
20
20
|
const registry_js_1 = require("../registry.js");
|
|
21
|
+
const types_js_1 = require("../types.js");
|
|
21
22
|
const context_js_1 = require("./context.js");
|
|
22
23
|
const fetch_owner_js_1 = require("./stages/fetch-owner.js");
|
|
23
24
|
const permission_gate_js_1 = require("./stages/permission-gate.js");
|
|
@@ -34,6 +35,7 @@ class RecordEngineImpl {
|
|
|
34
35
|
audit;
|
|
35
36
|
now;
|
|
36
37
|
onRecordDeleted;
|
|
38
|
+
listScope;
|
|
37
39
|
/** The FIXED write-stage order. Built once; identical for every write. */
|
|
38
40
|
writeStages;
|
|
39
41
|
constructor(deps) {
|
|
@@ -43,6 +45,7 @@ class RecordEngineImpl {
|
|
|
43
45
|
this.audit = deps.audit;
|
|
44
46
|
this.now = deps.nowMs ?? (() => Date.now());
|
|
45
47
|
this.onRecordDeleted = deps.onRecordDeleted;
|
|
48
|
+
this.listScope = deps.listScope;
|
|
46
49
|
// permission-gate + audit are ALWAYS present, in this order — no skip flag.
|
|
47
50
|
// fetch-owner runs FIRST (before the gate) so an OWNED update/delete's existing-row
|
|
48
51
|
// owner is available to decide() as context.attributes.ownerId (core owner baseline).
|
|
@@ -166,11 +169,24 @@ class RecordEngineImpl {
|
|
|
166
169
|
where = pk.columns.map((c) => `\`${c}\` = ?`).join(' AND ');
|
|
167
170
|
params = pk.columns.map((c) => id[c]);
|
|
168
171
|
}
|
|
169
|
-
// Tenant scoping: if the table has a tenant column, constrain to the actor's
|
|
172
|
+
// Tenant scoping: if the table has a tenant column, constrain to the actor's reachable
|
|
173
|
+
// subtree (ADR-0027 — active node + descendants), NOT just the self-node. ROOT spans all
|
|
174
|
+
// tenants — only a STRICT `=== true` isRoot counts (fail-closed). Fail-safe to the
|
|
175
|
+
// self-node if the subtree is malformed/absent (non-widening).
|
|
170
176
|
const tenantCol = this.tenantColumn(reg);
|
|
171
|
-
if (tenantCol) {
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
if (tenantCol && actor.isRoot !== true) {
|
|
178
|
+
const nodes = this.tenantScopeNodes(actor);
|
|
179
|
+
where += ` AND \`${tenantCol}\` IN (${nodes.map(() => '?').join(', ')})`;
|
|
180
|
+
params = [...params, ...nodes];
|
|
181
|
+
}
|
|
182
|
+
// Read-scope wall (ADR-0027, Task 4.1): for a type declaring `readScope` (the identity
|
|
183
|
+
// `system` types with no tenant column), a NON-ROOT read is bounded to the caller's
|
|
184
|
+
// reachable subtree. ANDs AFTER the tenant clause (which won't fire for these system
|
|
185
|
+
// types); root gets NO clause (spans all). Injection-free — only `?`-bound node ids.
|
|
186
|
+
const scope = this.readScopeClause(reg, actor);
|
|
187
|
+
if (scope) {
|
|
188
|
+
where += ` AND ${scope.sql}`;
|
|
189
|
+
params = [...params, ...scope.params];
|
|
174
190
|
}
|
|
175
191
|
if ((0, registry_js_1.isOwned)(reg)) {
|
|
176
192
|
// OWNED read — FETCH-THEN-GATE. Core's owner baseline needs the row's owner,
|
|
@@ -206,10 +222,39 @@ class RecordEngineImpl {
|
|
|
206
222
|
const clauses = [];
|
|
207
223
|
const params = [];
|
|
208
224
|
// Mandatory tenant scoping when the table is tenant-owned (cross-tenant safety).
|
|
225
|
+
// ROOT spans all tenants — but only a STRICT `=== true` isRoot counts (fail-closed):
|
|
226
|
+
// a truthy-non-boolean must never bypass the tenant boundary.
|
|
227
|
+
// SUBTREE-AWARE (ADR-0027): a non-root actor is bounded to its reachable subtree
|
|
228
|
+
// (active node + descendants), NOT just the self-node — `tenant_col IN (?, ...)` over
|
|
229
|
+
// `actor.scopeSubtreeNodeIds` (the SAME cached set the tenant gate + list-scope use).
|
|
230
|
+
// Fail-safe to `[actor.tenantId]` (self-node) if the subtree is malformed/absent.
|
|
231
|
+
//
|
|
232
|
+
// ⛔ ONE-AUTHORITY (Invariant #3, §10bis.5): this built-in clause is the FAIL-SAFE for
|
|
233
|
+
// when the list-scope engine is NOT wired (pre-Phase-3 / a consumer without it). When
|
|
234
|
+
// `listScope` IS wired, the list-scope FRAGMENT is the single source of the tenant
|
|
235
|
+
// boundary — it already emits `tenant_col = ?` / `IN (subtree)` for non-root AND ORs in
|
|
236
|
+
// the cross-tenant grant reach (§10bis.5). Emitting BOTH here would AND this narrower
|
|
237
|
+
// own-subtree filter over the fragment and KILL every cross-tenant-granted row before the
|
|
238
|
+
// fragment's widening can include it (the exact divergence the list≡decide gate caught).
|
|
239
|
+
// So when `listScope` is present we DEFER the tenant bound entirely to the fragment — one
|
|
240
|
+
// authority, no double-filter. (readScope system-types are unaffected: they have no
|
|
241
|
+
// tenant column, so this clause never fired for them anyway; their wall is `readScope`.)
|
|
209
242
|
const tenantCol = this.tenantColumn(reg);
|
|
210
|
-
if (tenantCol) {
|
|
211
|
-
|
|
212
|
-
|
|
243
|
+
if (tenantCol && actor.isRoot !== true && !this.listScope) {
|
|
244
|
+
const nodes = this.tenantScopeNodes(actor);
|
|
245
|
+
clauses.push(`\`${tenantCol}\` IN (${nodes.map(() => '?').join(', ')})`);
|
|
246
|
+
params.push(...nodes);
|
|
247
|
+
}
|
|
248
|
+
// Read-scope wall (ADR-0027, Task 4.1): a type declaring `readScope` (the identity
|
|
249
|
+
// `system` types with no tenant column) has its NON-ROOT reads bounded to the caller's
|
|
250
|
+
// reachable subtree. ANDed AFTER the tenant clause (which won't fire for these system
|
|
251
|
+
// types) into BOTH the data SELECT and the COUNT (shared clauses/params ⇒ consistent
|
|
252
|
+
// by construction). Root gets NO clause (spans all). Injection-free — only `?`-bound
|
|
253
|
+
// node ids; identifiers are the type's own column or a fixed framework template.
|
|
254
|
+
const readScope = this.readScopeClause(reg, actor);
|
|
255
|
+
if (readScope) {
|
|
256
|
+
clauses.push(readScope.sql);
|
|
257
|
+
params.push(...readScope.params);
|
|
213
258
|
}
|
|
214
259
|
// Equality filters on registered field columns only (unknown keys rejected).
|
|
215
260
|
// The COLUMN comes from a whitelisted field-key lookup; the VALUE is a bound
|
|
@@ -238,6 +283,20 @@ class RecordEngineImpl {
|
|
|
238
283
|
}
|
|
239
284
|
clauses.push(`(${ors.join(' OR ')})`);
|
|
240
285
|
}
|
|
286
|
+
// Record-level list scope (§8.3): AND the framework-computed visible-set fragment into
|
|
287
|
+
// BOTH the data SELECT and the COUNT (via the shared clauses/params — so `total` and
|
|
288
|
+
// `rows` stay consistent BY CONSTRUCTION). Runs only AFTER the coarse object-type gate
|
|
289
|
+
// (gateRead, at the TOP of list()) has allowed the read — the scope narrows rows WITHIN
|
|
290
|
+
// an allowed object-type list; it NEVER substitutes for the gate (the root/data-plane
|
|
291
|
+
// leak-guard). A scope-fn throw PROPAGATES (fail-closed — the read fails; we never fall
|
|
292
|
+
// back to unscoped rows). An empty `sql` is simply not ANDed (no fabricated widening).
|
|
293
|
+
if (this.listScope) {
|
|
294
|
+
const scope = await this.listScope(reg, actor, 'read');
|
|
295
|
+
if (scope.sql) {
|
|
296
|
+
clauses.push(scope.sql);
|
|
297
|
+
params.push(...scope.params);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
241
300
|
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
242
301
|
// Sort: resolve `sortBy` to a column the SAME whitelisted way as where-keys
|
|
243
302
|
// (unknown key → throw). The direction is an enum-checked literal (only 'desc'
|
|
@@ -265,6 +324,55 @@ class RecordEngineImpl {
|
|
|
265
324
|
const f = reg.fields.find((x) => x.trusted === 'tenantId');
|
|
266
325
|
return f ? (0, registry_js_1.columnOf)(f) : null;
|
|
267
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* The set of tenant nodes a non-root actor's reads are bounded to (ADR-0027): its
|
|
329
|
+
* active-scope subtree (active node + descendants), resolved once at login and cached on
|
|
330
|
+
* the principal (`scopeSubtreeNodeIds`). The engine's built-in tenant clause emits
|
|
331
|
+
* `tenant_col IN (these)` — the SAME set the tenant gate + list-scope use, so the three
|
|
332
|
+
* agree (no self-node collapse of the subtree). FAIL-SAFE + non-widening: a malformed or
|
|
333
|
+
* absent subtree falls back to just the actor's home node (`[actor.tenantId]`) — exactly
|
|
334
|
+
* today's flat self-tenant isolation; NEVER an empty set (which would hide all own rows).
|
|
335
|
+
*/
|
|
336
|
+
tenantScopeNodes(actor) {
|
|
337
|
+
const subtree = actor.scopeSubtreeNodeIds;
|
|
338
|
+
return Array.isArray(subtree) && subtree.length > 0 ? subtree : [actor.tenantId];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The read-scope predicate for a type's LIST/GET reads (ADR-0027, Task 4.1) — the wall
|
|
342
|
+
* that filters a `system` type's rows to the caller's reachable subtree, DECOUPLED from
|
|
343
|
+
* the write path (`readScope` NEVER stamps a write). Shared by list() AND get() so both
|
|
344
|
+
* apply the SAME wall. Returns `null` when there is nothing to AND:
|
|
345
|
+
* - the type declares no `readScope`, OR
|
|
346
|
+
* - the actor is ROOT (`isRoot === true`, strict) — root spans all, no clause.
|
|
347
|
+
* Otherwise ANDs, over the actor's subtree nodes (`tenantScopeNodes` — the SAME set the
|
|
348
|
+
* tenant clause + list-scope use, so the three agree):
|
|
349
|
+
* - `nodeColumn` → `` `<column>` IN (?, ...) `` — subtree ids `?`-bound.
|
|
350
|
+
* - `subtreeSubquery` → `` `<pkColumn>` IN (<subquerySql, :nodes → IN (?, ...)>) `` — the
|
|
351
|
+
* FIXED framework template with the subtree ids `?`-bound at `:nodes`.
|
|
352
|
+
*
|
|
353
|
+
* INJECTION-FREE BY CONSTRUCTION: the ONLY dynamic values are the `?`-bound subtree node
|
|
354
|
+
* ids; every identifier is a compile-time constant (the type's own column, or the fixed
|
|
355
|
+
* template). No node value is ever interpolated into the SQL string. FAIL-CLOSED: an
|
|
356
|
+
* empty subtree (shouldn't happen — `tenantScopeNodes` never returns empty) would still
|
|
357
|
+
* emit `IN ()`, which matches nothing (deny), never a widening clause.
|
|
358
|
+
*/
|
|
359
|
+
readScopeClause(reg, actor) {
|
|
360
|
+
const rs = reg.readScope;
|
|
361
|
+
if (!rs || actor.isRoot === true)
|
|
362
|
+
return null;
|
|
363
|
+
const nodes = this.tenantScopeNodes(actor);
|
|
364
|
+
const placeholders = nodes.map(() => '?').join(', ');
|
|
365
|
+
if (rs.kind === 'nodeColumn') {
|
|
366
|
+
return { sql: `\`${rs.column}\` IN (${placeholders})`, params: [...nodes] };
|
|
367
|
+
}
|
|
368
|
+
// subtreeSubquery: expand the FIXED template's `:nodes` token to a `?`-list. The
|
|
369
|
+
// template's identifiers are constants; only the node ids are `?`-bound (below).
|
|
370
|
+
const expanded = rs.subquerySql.replace(types_js_1.READ_SCOPE_NODES_TOKEN, placeholders);
|
|
371
|
+
return {
|
|
372
|
+
sql: `\`${rs.pkColumn}\` IN (${expanded})`,
|
|
373
|
+
params: [...nodes],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
268
376
|
}
|
|
269
377
|
exports.RecordEngineImpl = RecordEngineImpl;
|
|
270
378
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -98,6 +98,43 @@ export interface AccessDeclaration {
|
|
|
98
98
|
*/
|
|
99
99
|
readonly plane: CapabilityPlane;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* How a type's LIST/GET reads are scoped to the caller's reachable subtree (ADR-0027) —
|
|
103
|
+
* DECOUPLED from `trusted:'tenantId'` (which stamps WRITES via derive-write-context).
|
|
104
|
+
* `readScope` filters READS ONLY; it has ZERO write effect (a create still stamps only the
|
|
105
|
+
* `trusted` fields). Absent ⇒ no subtree read-scope (today's behavior — the tenant clause,
|
|
106
|
+
* if the type has a `trusted:'tenantId'` column, still applies; a `system` type with no
|
|
107
|
+
* such column then returns unscoped, which is why the identity `system` types declare a
|
|
108
|
+
* `readScope` to wall themselves). Applied by the engine AFTER the tenant clause, for
|
|
109
|
+
* NON-ROOT actors only (root spans all).
|
|
110
|
+
*
|
|
111
|
+
* Two forms:
|
|
112
|
+
* - `nodeColumn` — rows whose `<column>` is IN the caller's subtree node ids
|
|
113
|
+
* (`` `<column>` IN (?, ...) ``). Use when the type's own row carries the node itself
|
|
114
|
+
* (e.g. `company.c_uuid`, `company_membership.cm_company_uuid`).
|
|
115
|
+
* - `subtreeSubquery` — rows whose PK is IN a membership-derived subquery over the
|
|
116
|
+
* caller's subtree. The `subquerySql` is a FIXED, FRAMEWORK-AUTHORED template per type
|
|
117
|
+
* (NEVER client SQL): every identifier is a compile-time constant, and the ONLY dynamic,
|
|
118
|
+
* `?`-bound values are the subtree node ids — injected via the `:nodes` placeholder the
|
|
119
|
+
* engine expands to `IN (?, ?, ...)`. This is injection-free BY CONSTRUCTION (no value
|
|
120
|
+
* is ever interpolated into the SQL string).
|
|
121
|
+
*/
|
|
122
|
+
export type ReadScope = {
|
|
123
|
+
readonly kind: 'nodeColumn';
|
|
124
|
+
readonly column: string;
|
|
125
|
+
} | {
|
|
126
|
+
readonly kind: 'subtreeSubquery';
|
|
127
|
+
/** The type's OWN PK column to test membership of. */
|
|
128
|
+
readonly pkColumn: string;
|
|
129
|
+
/**
|
|
130
|
+
* A FIXED framework template containing the `:nodes` placeholder (expanded by the
|
|
131
|
+
* engine to `IN (?, ?, ...)` with the subtree ids `?`-bound). NOT client SQL —
|
|
132
|
+
* identifiers are constants; the ONLY dynamic values are the bound node ids.
|
|
133
|
+
*/
|
|
134
|
+
readonly subquerySql: string;
|
|
135
|
+
};
|
|
136
|
+
/** The token a `subtreeSubquery` template uses for the engine-expanded subtree id list. */
|
|
137
|
+
export declare const READ_SCOPE_NODES_TOKEN = ":nodes";
|
|
101
138
|
/** Primary-key strategy for a registered table. */
|
|
102
139
|
export type PkStrategy = {
|
|
103
140
|
readonly kind: 'uuid';
|
|
@@ -137,6 +174,13 @@ export interface ObjectType {
|
|
|
137
174
|
* fails (chokepoint 1). See {@link AccessDeclaration}.
|
|
138
175
|
*/
|
|
139
176
|
readonly access: AccessDeclaration;
|
|
177
|
+
/**
|
|
178
|
+
* OPTIONAL read-scope declaration (ADR-0027) — how LIST/GET reads are filtered to the
|
|
179
|
+
* caller's reachable subtree, DECOUPLED from the write path. See {@link ReadScope}.
|
|
180
|
+
* Absent ⇒ no subtree read-scope (the tenant clause, if any, still applies). Has ZERO
|
|
181
|
+
* write effect.
|
|
182
|
+
*/
|
|
183
|
+
readonly readScope?: ReadScope;
|
|
140
184
|
}
|
|
141
185
|
/** Input to a create/update — field keys → values (+ optional junction refs). */
|
|
142
186
|
export interface RecordInput {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/D,iFAAiF;AACjF,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,MAAM,GACN,WAAW,GACX,MAAM,CAAC;AAEX,qDAAqD;AACrD,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC;CAC9B;AAED,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;AAEnD;;iFAEiF;AACjF,MAAM,MAAM,YAAY,GACpB,YAAY,GACZ,UAAU,GACV,UAAU,GACV,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,gBAAgB,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CACjC;AAED,mDAAmD;AACnD,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAExE,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC9D;AAED,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACzB,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2BAA2B;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C,mEAAmE;IACnE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -13,3 +13,6 @@
|
|
|
13
13
|
* surface.
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.READ_SCOPE_NODES_TOKEN = void 0;
|
|
17
|
+
/** The token a `subtreeSubquery` template uses for the engine-expanded subtree id list. */
|
|
18
|
+
exports.READ_SCOPE_NODES_TOKEN = ':nodes';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the paved road. ONE unbypassable write pipe (resolve-schema → permission gate → derive-write-context → validate → compute → write → junction-sync → audit → emit-event) over real physical tables, that every feature's CRUD flows through. Feature code receives ONLY the RecordEngine facade — never a raw pool/DataSource/QueryRunner (the physical wall). Security-gate + audit are mandatory stages. Missing capability → a typed CapabilityNotSupportedError (the gap-flag STOP). Builds on @astram/core (metadata contract, decide(), audit) + @astram/guard (decision authority).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"mariadb": "^3.4.0",
|
|
24
|
-
"@astram/core": "1.
|
|
24
|
+
"@astram/core": "1.2.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@nestjs/common": "^11",
|