@almadar/runtime 6.63.0 → 6.64.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/{OrbitalServerRuntime-nqwGldMC.d.ts → OrbitalServerRuntime-DOJDoqXu.d.ts} +23 -0
- package/dist/OrbitalServerRuntime.d.ts +1 -1
- package/dist/OrbitalServerRuntime.js +55 -6
- package/dist/{chunk-C7SIRVGS.js → chunk-IFSFX43J.js} +145 -9
- package/dist/index.d.ts +53 -2
- package/dist/index.js +2 -2
- package/package.json +3 -3
|
@@ -1416,6 +1416,29 @@ declare class OrbitalServerRuntime {
|
|
|
1416
1416
|
* instead of re-seeding.
|
|
1417
1417
|
*/
|
|
1418
1418
|
setDefaultUser(user: UserContext | undefined): void;
|
|
1419
|
+
/** setDefaultUser can land before register() finishes resolving the schema
|
|
1420
|
+
* (a dev host pins a persona right after boot). An ungated restamp there
|
|
1421
|
+
* would hand a read-only persona authorship of drafts, so the restamp is
|
|
1422
|
+
* DEFERRED until the schema exists — register()/registerSync() flush it. */
|
|
1423
|
+
private pendingOwnerRestamp;
|
|
1424
|
+
/**
|
|
1425
|
+
* Re-point the mock seeder's owner stamps at `user`. The adapter's
|
|
1426
|
+
* installed owner gate (see installOwnerGate) decides per row whether the
|
|
1427
|
+
* persona could have CREATED it — a reader keeps seeing the store's
|
|
1428
|
+
* published face instead of inheriting draft ownership from the demo
|
|
1429
|
+
* stamper. Deferred when the schema (and so the gate) isn't resolved yet.
|
|
1430
|
+
*/
|
|
1431
|
+
private restampOwnerGated;
|
|
1432
|
+
/**
|
|
1433
|
+
* Install the schema's `@create` directives as the mock seeder's owner
|
|
1434
|
+
* gate — the single authority on who may author a row. Consulted by BOTH
|
|
1435
|
+
* `restampOwner` (persona switches) and `seed()` (a client connect
|
|
1436
|
+
* re-registers, and the deterministic reseed re-stamps with the current
|
|
1437
|
+
* ownerId — gating only the switch path would leak right there). Reads the
|
|
1438
|
+
* CURRENT default user at evaluation time so one installation covers every
|
|
1439
|
+
* later switch. Then flushes any switch that arrived before the schema.
|
|
1440
|
+
*/
|
|
1441
|
+
private installOwnerGate;
|
|
1419
1442
|
/** The viewer a dev host is currently presenting the app as. */
|
|
1420
1443
|
getDefaultUser(): UserContext | undefined;
|
|
1421
1444
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import 'express';
|
|
2
|
-
export { F as ClientEffectTuple, G as ClientNavigateBackTuple, H as ClientNavigateTuple, J as ClientNotifyTuple, K as ClientRenderUITuple, M as EffectResult, e as InMemoryPersistence, N as LiveBroadcastItem, Q as LoaderConfig, O as OrbitalEventRequest, f as OrbitalEventResponse, T as OrbitalServerRuntime, g as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, V as RuntimeTraitTick, p as collectDeclaredConfigDefaults, W as createOrbitalServerRuntime } from './OrbitalServerRuntime-
|
|
2
|
+
export { F as ClientEffectTuple, G as ClientNavigateBackTuple, H as ClientNavigateTuple, J as ClientNotifyTuple, K as ClientRenderUITuple, M as EffectResult, e as InMemoryPersistence, N as LiveBroadcastItem, Q as LoaderConfig, O as OrbitalEventRequest, f as OrbitalEventResponse, T as OrbitalServerRuntime, g as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, V as RuntimeTraitTick, p as collectDeclaredConfigDefaults, W as createOrbitalServerRuntime } from './OrbitalServerRuntime-DOJDoqXu.js';
|
|
3
3
|
import './types-BaD_ox7e.js';
|
|
4
4
|
import '@almadar/core';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-
|
|
2
|
-
export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-
|
|
1
|
+
import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-IFSFX43J.js';
|
|
2
|
+
export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-IFSFX43J.js';
|
|
3
3
|
import { isValidCronExpression } from './chunk-OU3ITB5S.js';
|
|
4
|
-
import { createContextFromBindings, resolveCallSitePayloadCaptures, applyRowAccess,
|
|
4
|
+
import { createContextFromBindings, checkMutationAccess, resolveCallSitePayloadCaptures, applyRowAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
|
|
5
5
|
import './chunk-T4VDAB4C.js';
|
|
6
6
|
import './chunk-SCRAHWOC.js';
|
|
7
7
|
import './chunk-MLKGABMK.js';
|
|
@@ -9,7 +9,7 @@ import { createLogger } from '@almadar/logger';
|
|
|
9
9
|
import * as nodeModule from 'module';
|
|
10
10
|
import { evaluateListenPayloadExpr, evaluateGuard, evaluate } from '@almadar/evaluator';
|
|
11
11
|
import { DEFAULT_VIEWER, buildResolvedTraitConfigs, isInlineTrait, isEntityCall, applyListenPayloadMapping, personaFromIdentityRow, normalizeUserContext, isRuntimeEntity, isPageReference } from '@almadar/core';
|
|
12
|
-
import { ownerFieldsFromSchema, identityEntityName, entityAccessPolicies } from '@almadar/core/mock';
|
|
12
|
+
import { ownerFieldsFromSchema, identityEntityName, entityAccessPoliciesByStoreKey, entityAccessPolicies } from '@almadar/core/mock';
|
|
13
13
|
import { getPatternFieldsContract } from '@almadar/core/patterns';
|
|
14
14
|
|
|
15
15
|
// src/identity/routing.ts
|
|
@@ -355,6 +355,7 @@ var OrbitalServerRuntime = class {
|
|
|
355
355
|
this.setupTicks();
|
|
356
356
|
this.resolvedSchema = schema;
|
|
357
357
|
this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
|
|
358
|
+
this.installOwnerGate();
|
|
358
359
|
}
|
|
359
360
|
/**
|
|
360
361
|
* Teach the mock seeder which columns hold the viewer's id, derived from the
|
|
@@ -391,7 +392,7 @@ var OrbitalServerRuntime = class {
|
|
|
391
392
|
this.setupTicks();
|
|
392
393
|
this.resolvedSchema = schema;
|
|
393
394
|
this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
|
|
394
|
-
this.
|
|
395
|
+
this.installOwnerGate();
|
|
395
396
|
}
|
|
396
397
|
/**
|
|
397
398
|
* Returns the schema that this runtime is currently executing, post-
|
|
@@ -1065,7 +1066,55 @@ var OrbitalServerRuntime = class {
|
|
|
1065
1066
|
const previousId = this.config.defaultUser?.id;
|
|
1066
1067
|
this.config.defaultUser = user;
|
|
1067
1068
|
if (this.persistence instanceof MockPersistenceAdapter && user?.id !== void 0 && user.id !== previousId) {
|
|
1068
|
-
this.
|
|
1069
|
+
this.restampOwnerGated(user);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/** setDefaultUser can land before register() finishes resolving the schema
|
|
1073
|
+
* (a dev host pins a persona right after boot). An ungated restamp there
|
|
1074
|
+
* would hand a read-only persona authorship of drafts, so the restamp is
|
|
1075
|
+
* DEFERRED until the schema exists — register()/registerSync() flush it. */
|
|
1076
|
+
pendingOwnerRestamp = false;
|
|
1077
|
+
/**
|
|
1078
|
+
* Re-point the mock seeder's owner stamps at `user`. The adapter's
|
|
1079
|
+
* installed owner gate (see installOwnerGate) decides per row whether the
|
|
1080
|
+
* persona could have CREATED it — a reader keeps seeing the store's
|
|
1081
|
+
* published face instead of inheriting draft ownership from the demo
|
|
1082
|
+
* stamper. Deferred when the schema (and so the gate) isn't resolved yet.
|
|
1083
|
+
*/
|
|
1084
|
+
restampOwnerGated(user) {
|
|
1085
|
+
if (!(this.persistence instanceof MockPersistenceAdapter)) return;
|
|
1086
|
+
if (!this.resolvedSchema) {
|
|
1087
|
+
this.pendingOwnerRestamp = true;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
this.persistence.restampOwner(user.id);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Install the schema's `@create` directives as the mock seeder's owner
|
|
1094
|
+
* gate — the single authority on who may author a row. Consulted by BOTH
|
|
1095
|
+
* `restampOwner` (persona switches) and `seed()` (a client connect
|
|
1096
|
+
* re-registers, and the deterministic reseed re-stamps with the current
|
|
1097
|
+
* ownerId — gating only the switch path would leak right there). Reads the
|
|
1098
|
+
* CURRENT default user at evaluation time so one installation covers every
|
|
1099
|
+
* later switch. Then flushes any switch that arrived before the schema.
|
|
1100
|
+
*/
|
|
1101
|
+
installOwnerGate() {
|
|
1102
|
+
if (!(this.persistence instanceof MockPersistenceAdapter) || !this.resolvedSchema) return;
|
|
1103
|
+
const policiesByStore = entityAccessPoliciesByStoreKey(this.resolvedSchema);
|
|
1104
|
+
this.persistence.setOwnerGate((storeKey, candidateRow) => {
|
|
1105
|
+
const user = this.config.defaultUser;
|
|
1106
|
+
if (!user) return true;
|
|
1107
|
+
return checkMutationAccess(candidateRow, policiesByStore.get(storeKey)?.create, { user });
|
|
1108
|
+
});
|
|
1109
|
+
this.persistence.setOwnerCandidateGate((storeKey, candidateRow, candidateIdentityRow) => {
|
|
1110
|
+
const persona = personaFromIdentityRow(candidateIdentityRow);
|
|
1111
|
+
if (!persona) return false;
|
|
1112
|
+
return checkMutationAccess(candidateRow, policiesByStore.get(storeKey)?.create, { user: persona });
|
|
1113
|
+
});
|
|
1114
|
+
if (this.pendingOwnerRestamp) {
|
|
1115
|
+
this.pendingOwnerRestamp = false;
|
|
1116
|
+
const user = this.config.defaultUser;
|
|
1117
|
+
if (user?.id !== void 0) this.restampOwnerGated(user);
|
|
1069
1118
|
}
|
|
1070
1119
|
}
|
|
1071
1120
|
/** The viewer a dev host is currently presenting the app as. */
|
|
@@ -2128,6 +2128,12 @@ var mockLog = createLogger("almadar:runtime:mock");
|
|
|
2128
2128
|
var DEFAULT_MOCK_SEED = 42;
|
|
2129
2129
|
var SEED_REFERENCE_TIMESTAMP = "2024-01-01T00:00:00.000Z";
|
|
2130
2130
|
var MS_PER_DAY = 864e5;
|
|
2131
|
+
function isRelationField(field) {
|
|
2132
|
+
return field.type === "relation";
|
|
2133
|
+
}
|
|
2134
|
+
function stampedCellKey(entity, id, column) {
|
|
2135
|
+
return `${entity}\0${id}\0${column}`;
|
|
2136
|
+
}
|
|
2131
2137
|
var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
2132
2138
|
stores = /* @__PURE__ */ new Map();
|
|
2133
2139
|
schemas = /* @__PURE__ */ new Map();
|
|
@@ -2191,21 +2197,108 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2191
2197
|
* No-op when there is no new id, or it matches the current one — an
|
|
2192
2198
|
* anonymous switch (`newOwnerId === undefined`) leaves existing stamps as
|
|
2193
2199
|
* they are rather than clearing a non-nullable owner column.
|
|
2200
|
+
*
|
|
2201
|
+
* Both this re-labeling AND `seed()`'s original stamping consult the
|
|
2202
|
+
* `ownerGate` hook (see `setOwnerGate`) — a re-register reseeds identical
|
|
2203
|
+
* rows, so gating only the restamp path leaks the moment a client
|
|
2204
|
+
* connects and the deterministic reseed stamps with the updated ownerId.
|
|
2194
2205
|
*/
|
|
2195
2206
|
restampOwner(newOwnerId) {
|
|
2196
2207
|
const oldOwnerId = this.config.ownerId;
|
|
2197
2208
|
this.config.ownerId = newOwnerId;
|
|
2198
2209
|
if (!newOwnerId || newOwnerId === oldOwnerId) return;
|
|
2210
|
+
let skipped = 0;
|
|
2211
|
+
let fallbackStamps = 0;
|
|
2199
2212
|
for (const cell of this.ownerStampedCells) {
|
|
2200
2213
|
const row = this.stores.get(cell.entity)?.get(cell.id);
|
|
2201
|
-
if (row)
|
|
2214
|
+
if (!row) continue;
|
|
2215
|
+
if (this.ownerGate && !this.ownerGate(cell.entity, { ...row, [cell.column]: newOwnerId })) {
|
|
2216
|
+
const fallbackId = this.firstEligibleOwner(cell.entity, cell.column, row);
|
|
2217
|
+
if (fallbackId === void 0) {
|
|
2218
|
+
skipped++;
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
row[cell.column] = fallbackId;
|
|
2222
|
+
fallbackStamps++;
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
row[cell.column] = newOwnerId;
|
|
2202
2226
|
}
|
|
2203
2227
|
mockLog.debug("mock:owner-restamped", {
|
|
2204
2228
|
from: oldOwnerId,
|
|
2205
2229
|
to: newOwnerId,
|
|
2206
|
-
cells: this.ownerStampedCells.length
|
|
2230
|
+
cells: this.ownerStampedCells.length,
|
|
2231
|
+
skipped,
|
|
2232
|
+
fallbackStamps
|
|
2207
2233
|
});
|
|
2208
2234
|
}
|
|
2235
|
+
/**
|
|
2236
|
+
* The host's answer to "may the current viewer OWN this row?" — evaluated
|
|
2237
|
+
* against the candidate row with the owner column already re-pointed,
|
|
2238
|
+
* `storeKey` being the collection key the stores are keyed by. The runtime
|
|
2239
|
+
* installs the registered schema's `@create` directive here (the single
|
|
2240
|
+
* authority on who authors; no role heuristics), so a read-only persona
|
|
2241
|
+
* never inherits authorship of drafts from the demo stamper — neither on a
|
|
2242
|
+
* persona switch (`restampOwner`) nor on a deterministic reseed (`seed`).
|
|
2243
|
+
* Absent hook = stamp unconditionally (pre-role-model behavior).
|
|
2244
|
+
*/
|
|
2245
|
+
ownerGate;
|
|
2246
|
+
setOwnerGate(gate) {
|
|
2247
|
+
this.ownerGate = gate;
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* The host's answer to "may THIS identity row own this candidate row?" —
|
|
2251
|
+
* evaluated per candidate rather than against the fixed default viewer, so
|
|
2252
|
+
* `linkRelationFields` and the eligible-owner fallback (see
|
|
2253
|
+
* `firstEligibleOwner`) can pick a policy-valid owner from the whole
|
|
2254
|
+
* identity roster instead of only ever checking the current default user.
|
|
2255
|
+
* Absent hook = no candidate filtering (pre-role-model behavior).
|
|
2256
|
+
*/
|
|
2257
|
+
ownerCandidateGate;
|
|
2258
|
+
setOwnerCandidateGate(gate) {
|
|
2259
|
+
this.ownerCandidateGate = gate;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* The store an owner column's relation field targets, resolved the same
|
|
2263
|
+
* way `linkRelationFields` resolves any relation target (id sibling wins
|
|
2264
|
+
* over the `entity` name string) — but keyed by STORE, mirroring that
|
|
2265
|
+
* method's first-declarer-per-collection rule, so `seed()` and
|
|
2266
|
+
* `restampOwner()` can find it before a `linkRelationFields` pass has run
|
|
2267
|
+
* this registration. Undefined when no registered schema on `storeKey`
|
|
2268
|
+
* declares `column` as a relation field.
|
|
2269
|
+
*/
|
|
2270
|
+
resolveOwnerColumnTargetStore(storeKey, column) {
|
|
2271
|
+
for (const [normalizedName, schema] of this.schemas) {
|
|
2272
|
+
if ((this.storeKeyByEntity.get(normalizedName) ?? normalizedName) !== storeKey) continue;
|
|
2273
|
+
const field = schema.fields.find((f) => f.name === column);
|
|
2274
|
+
if (field && isRelationField(field)) {
|
|
2275
|
+
return (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? this.resolveStoreKey(field.relation.entity);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return void 0;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* The first identity-store row (seed order) whose persona is eligible, per
|
|
2282
|
+
* the installed `ownerCandidateGate`, to own `row`'s `column` in `storeKey`
|
|
2283
|
+
* — the deterministic replacement for "skip the stamp" when the CURRENT
|
|
2284
|
+
* viewer fails the `@create` policy (e.g. a reader-default persona against
|
|
2285
|
+
* an author|reviewer-only policy). Undefined when no gate is installed, the
|
|
2286
|
+
* column isn't a resolvable relation, or no identity row is eligible — the
|
|
2287
|
+
* caller keeps today's skip behavior in that case.
|
|
2288
|
+
*/
|
|
2289
|
+
firstEligibleOwner(storeKey, column, row) {
|
|
2290
|
+
if (!this.ownerCandidateGate) return void 0;
|
|
2291
|
+
const identityStoreKey = this.resolveOwnerColumnTargetStore(storeKey, column);
|
|
2292
|
+
const identityStore = identityStoreKey ? this.stores.get(identityStoreKey) : void 0;
|
|
2293
|
+
if (!identityStore) return void 0;
|
|
2294
|
+
for (const identityRow of identityStore.values()) {
|
|
2295
|
+
const candidateId = identityRow["id"];
|
|
2296
|
+
if (this.ownerCandidateGate(storeKey, { ...row, [column]: candidateId }, identityRow)) {
|
|
2297
|
+
return candidateId;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return void 0;
|
|
2301
|
+
}
|
|
2209
2302
|
/** Re-anchor the PRNG to the configured seed. Called before every
|
|
2210
2303
|
* re-seed loop so identical reseed sequences produce identical rows
|
|
2211
2304
|
* (timestamps + generated fields). Without this, the first
|
|
@@ -2305,16 +2398,28 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2305
2398
|
relationFieldsByStore.set(storeKey, fieldMap);
|
|
2306
2399
|
}
|
|
2307
2400
|
for (const f of schema.fields) {
|
|
2308
|
-
if ((
|
|
2401
|
+
if (isRelationField(f) && !fieldMap.has(f.name)) {
|
|
2309
2402
|
fieldMap.set(f.name, f);
|
|
2310
2403
|
}
|
|
2311
2404
|
}
|
|
2312
2405
|
}
|
|
2406
|
+
const ownerColumnsByStore = /* @__PURE__ */ new Map();
|
|
2407
|
+
for (const { entity, field } of this.parsedOwnerFields()) {
|
|
2408
|
+
const storeKey = this.resolveStoreKey(entity);
|
|
2409
|
+
const set = ownerColumnsByStore.get(storeKey) ?? /* @__PURE__ */ new Set();
|
|
2410
|
+
set.add(field);
|
|
2411
|
+
ownerColumnsByStore.set(storeKey, set);
|
|
2412
|
+
}
|
|
2413
|
+
const stampedCellKeys = new Set(
|
|
2414
|
+
this.ownerStampedCells.map((cell) => stampedCellKey(cell.entity, cell.id, cell.column))
|
|
2415
|
+
);
|
|
2313
2416
|
for (const [storeKey, fieldMap] of relationFieldsByStore) {
|
|
2314
2417
|
const store = this.stores.get(storeKey);
|
|
2315
2418
|
if (!store || fieldMap.size === 0) continue;
|
|
2316
2419
|
const rows = [...store.values()];
|
|
2420
|
+
const ownerColumns = ownerColumnsByStore.get(storeKey);
|
|
2317
2421
|
for (const field of fieldMap.values()) {
|
|
2422
|
+
const isOwnerColumn = ownerColumns?.has(field.name) ?? false;
|
|
2318
2423
|
const targetKey = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? this.resolveStoreKey(field.relation.entity);
|
|
2319
2424
|
const targetStore = this.stores.get(targetKey);
|
|
2320
2425
|
if (!targetStore || targetStore.size === 0) continue;
|
|
@@ -2322,7 +2427,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2322
2427
|
const cardinality = field.relation.cardinality ?? "many";
|
|
2323
2428
|
if (sameStore && (cardinality === "one" || cardinality === "many-to-one")) {
|
|
2324
2429
|
rows.forEach((row, i) => {
|
|
2325
|
-
if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId) {
|
|
2430
|
+
if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId || stampedCellKeys.has(stampedCellKey(storeKey, row["id"], field.name))) {
|
|
2326
2431
|
return;
|
|
2327
2432
|
}
|
|
2328
2433
|
row[field.name] = i === 0 ? "" : rows[Math.floor((i - 1) / 2)]["id"];
|
|
@@ -2330,15 +2435,21 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2330
2435
|
continue;
|
|
2331
2436
|
}
|
|
2332
2437
|
for (const row of rows) {
|
|
2333
|
-
|
|
2438
|
+
const selfId = row["id"];
|
|
2439
|
+
if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId || stampedCellKeys.has(stampedCellKey(storeKey, selfId, field.name))) {
|
|
2334
2440
|
continue;
|
|
2335
2441
|
}
|
|
2336
|
-
|
|
2337
|
-
const eligible = [];
|
|
2442
|
+
let eligible = [];
|
|
2338
2443
|
for (const id of targetStore.keys()) {
|
|
2339
2444
|
if (sameStore && id === selfId) continue;
|
|
2340
2445
|
eligible.push(id);
|
|
2341
2446
|
}
|
|
2447
|
+
if (isOwnerColumn && this.ownerCandidateGate) {
|
|
2448
|
+
const gate = this.ownerCandidateGate;
|
|
2449
|
+
eligible = eligible.filter(
|
|
2450
|
+
(id) => gate(storeKey, { ...row, [field.name]: id }, targetStore.get(id))
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2342
2453
|
if (eligible.length === 0) continue;
|
|
2343
2454
|
if (cardinality === "one" || cardinality === "many-to-one") {
|
|
2344
2455
|
row[field.name] = randomArrayElement(eligible);
|
|
@@ -2416,13 +2527,22 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2416
2527
|
if (this.config.debug) {
|
|
2417
2528
|
mockLog.debug("seeding", { count, entity: entityName });
|
|
2418
2529
|
}
|
|
2419
|
-
const ownerCols =
|
|
2530
|
+
const ownerCols = this.parsedOwnerFields().filter(({ entity }) => entity.toLowerCase() === normalized).map(({ field }) => field);
|
|
2420
2531
|
const ownerId = this.config.ownerId;
|
|
2421
2532
|
const generated = [];
|
|
2533
|
+
let fallbackStamps = 0;
|
|
2422
2534
|
for (let i = 0; i < count; i++) {
|
|
2423
2535
|
const item = this.generateMockItem(entityName, fields, i + 1, persistence);
|
|
2424
2536
|
if (ownerId && ownerCols.length > 0 && i % 2 === 0) {
|
|
2425
2537
|
for (const col of ownerCols) {
|
|
2538
|
+
if (this.ownerGate && !this.ownerGate(storeKey, { ...item, [col]: ownerId })) {
|
|
2539
|
+
const fallbackId = this.firstEligibleOwner(storeKey, col, item);
|
|
2540
|
+
if (fallbackId === void 0) continue;
|
|
2541
|
+
item[col] = fallbackId;
|
|
2542
|
+
this.ownerStampedCells.push({ entity: storeKey, id: item.id, column: col });
|
|
2543
|
+
fallbackStamps++;
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2426
2546
|
item[col] = ownerId;
|
|
2427
2547
|
this.ownerStampedCells.push({ entity: storeKey, id: item.id, column: col });
|
|
2428
2548
|
}
|
|
@@ -2433,7 +2553,23 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
|
|
|
2433
2553
|
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : ""
|
|
2434
2554
|
});
|
|
2435
2555
|
}
|
|
2436
|
-
mockLog.debug("mock:seed", () => ({
|
|
2556
|
+
mockLog.debug("mock:seed", () => ({
|
|
2557
|
+
entityName,
|
|
2558
|
+
count,
|
|
2559
|
+
idsAndTimestamps: JSON.stringify(generated),
|
|
2560
|
+
fallbackStamps
|
|
2561
|
+
}));
|
|
2562
|
+
}
|
|
2563
|
+
/** Parsed `config.ownerFields` pairs — every declared "Entity.field" owner
|
|
2564
|
+
* column, split once so `seed()` and `linkRelationFields()` don't each
|
|
2565
|
+
* re-parse the same strings. */
|
|
2566
|
+
parsedOwnerFields() {
|
|
2567
|
+
const out = [];
|
|
2568
|
+
for (const pair of this.config.ownerFields ?? []) {
|
|
2569
|
+
const [entity, field] = pair.split(".");
|
|
2570
|
+
if (entity && field) out.push({ entity, field });
|
|
2571
|
+
}
|
|
2572
|
+
return out;
|
|
2437
2573
|
}
|
|
2438
2574
|
/**
|
|
2439
2575
|
* Generate a single mock item. Field values come from the canonical policy in
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { g as RuntimePatternValue, B as BindingContext, f as EvaluationContextExtensions, P as PatternProps, E as EffectHandlers, h as EffectContext, i as ExecutionEnvironment, j as EffectResult, S as ServiceCallContext, T as TraitDefinition } from './types-BaD_ox7e.js';
|
|
2
2
|
export { k as BrowserFileMeta, l as BrowserFilePickerOptions, m as BrowserGeolocationOptions, n as BrowserGeolocationPosition, C as ConfigContext, o as Effect, a as EventListener, H as HANDLER_MANIFEST, I as IEventBus, b as RuntimeConfig, R as RuntimeEvent, d as TraitState, c as TransitionObserver, e as TransitionResult, U as Unsubscribe } from './types-BaD_ox7e.js';
|
|
3
|
-
import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital, P as PersistenceAdapter } from './OrbitalServerRuntime-
|
|
4
|
-
export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as collectDeclaredConfigDefaults, q as collectDeclaredEntityDefaults, r as createInitialTraitState, s as findInitialState, t as findTransition, u as getIsolatedCollectionName, v as getNamespacedEvent, w as isBrowser, x as isElectron, y as isNamespacedEvent, z as isNode, A as normalizeEventKey, B as parseNamespacedEvent, C as preprocessSchema, D as processEvent } from './OrbitalServerRuntime-
|
|
3
|
+
import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital, P as PersistenceAdapter } from './OrbitalServerRuntime-DOJDoqXu.js';
|
|
4
|
+
export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as collectDeclaredConfigDefaults, q as collectDeclaredEntityDefaults, r as createInitialTraitState, s as findInitialState, t as findTransition, u as getIsolatedCollectionName, v as getNamespacedEvent, w as isBrowser, x as isElectron, y as isNamespacedEvent, z as isNode, A as normalizeEventKey, B as parseNamespacedEvent, C as preprocessSchema, D as processEvent } from './OrbitalServerRuntime-DOJDoqXu.js';
|
|
5
5
|
import { EvaluationContext, SExpressionEvaluator } from '@almadar/evaluator';
|
|
6
6
|
export { EvaluationContext, createMinimalContext } from '@almadar/evaluator';
|
|
7
7
|
import { RenderBindingMarker, SExpr, RuntimeValue, TraitConfigObject, EventPayload, PatternConfig, EntityId, EntityField, EntityRow, EntityPersistence, ServiceParams, EntityAccessPolicies, PayloadField, OrbitalDefinition, OrbitalSchema } from '@almadar/core';
|
|
@@ -663,8 +663,55 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
|
|
|
663
663
|
* No-op when there is no new id, or it matches the current one — an
|
|
664
664
|
* anonymous switch (`newOwnerId === undefined`) leaves existing stamps as
|
|
665
665
|
* they are rather than clearing a non-nullable owner column.
|
|
666
|
+
*
|
|
667
|
+
* Both this re-labeling AND `seed()`'s original stamping consult the
|
|
668
|
+
* `ownerGate` hook (see `setOwnerGate`) — a re-register reseeds identical
|
|
669
|
+
* rows, so gating only the restamp path leaks the moment a client
|
|
670
|
+
* connects and the deterministic reseed stamps with the updated ownerId.
|
|
666
671
|
*/
|
|
667
672
|
restampOwner(newOwnerId: string | undefined): void;
|
|
673
|
+
/**
|
|
674
|
+
* The host's answer to "may the current viewer OWN this row?" — evaluated
|
|
675
|
+
* against the candidate row with the owner column already re-pointed,
|
|
676
|
+
* `storeKey` being the collection key the stores are keyed by. The runtime
|
|
677
|
+
* installs the registered schema's `@create` directive here (the single
|
|
678
|
+
* authority on who authors; no role heuristics), so a read-only persona
|
|
679
|
+
* never inherits authorship of drafts from the demo stamper — neither on a
|
|
680
|
+
* persona switch (`restampOwner`) nor on a deterministic reseed (`seed`).
|
|
681
|
+
* Absent hook = stamp unconditionally (pre-role-model behavior).
|
|
682
|
+
*/
|
|
683
|
+
private ownerGate?;
|
|
684
|
+
setOwnerGate(gate: ((storeKey: string, candidateRow: EntityRow) => boolean) | undefined): void;
|
|
685
|
+
/**
|
|
686
|
+
* The host's answer to "may THIS identity row own this candidate row?" —
|
|
687
|
+
* evaluated per candidate rather than against the fixed default viewer, so
|
|
688
|
+
* `linkRelationFields` and the eligible-owner fallback (see
|
|
689
|
+
* `firstEligibleOwner`) can pick a policy-valid owner from the whole
|
|
690
|
+
* identity roster instead of only ever checking the current default user.
|
|
691
|
+
* Absent hook = no candidate filtering (pre-role-model behavior).
|
|
692
|
+
*/
|
|
693
|
+
private ownerCandidateGate?;
|
|
694
|
+
setOwnerCandidateGate(gate: ((storeKey: string, candidateRow: EntityRow, candidateIdentityRow: EntityRow) => boolean) | undefined): void;
|
|
695
|
+
/**
|
|
696
|
+
* The store an owner column's relation field targets, resolved the same
|
|
697
|
+
* way `linkRelationFields` resolves any relation target (id sibling wins
|
|
698
|
+
* over the `entity` name string) — but keyed by STORE, mirroring that
|
|
699
|
+
* method's first-declarer-per-collection rule, so `seed()` and
|
|
700
|
+
* `restampOwner()` can find it before a `linkRelationFields` pass has run
|
|
701
|
+
* this registration. Undefined when no registered schema on `storeKey`
|
|
702
|
+
* declares `column` as a relation field.
|
|
703
|
+
*/
|
|
704
|
+
private resolveOwnerColumnTargetStore;
|
|
705
|
+
/**
|
|
706
|
+
* The first identity-store row (seed order) whose persona is eligible, per
|
|
707
|
+
* the installed `ownerCandidateGate`, to own `row`'s `column` in `storeKey`
|
|
708
|
+
* — the deterministic replacement for "skip the stamp" when the CURRENT
|
|
709
|
+
* viewer fails the `@create` policy (e.g. a reader-default persona against
|
|
710
|
+
* an author|reviewer-only policy). Undefined when no gate is installed, the
|
|
711
|
+
* column isn't a resolvable relation, or no identity row is eligible — the
|
|
712
|
+
* caller keeps today's skip behavior in that case.
|
|
713
|
+
*/
|
|
714
|
+
private firstEligibleOwner;
|
|
668
715
|
/** Re-anchor the PRNG to the configured seed. Called before every
|
|
669
716
|
* re-seed loop so identical reseed sequences produce identical rows
|
|
670
717
|
* (timestamps + generated fields). Without this, the first
|
|
@@ -722,6 +769,10 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
|
|
|
722
769
|
* Seed an entity with mock data.
|
|
723
770
|
*/
|
|
724
771
|
seed(entityName: string, fields: EntityField[], count: number, persistence?: EntityPersistence): void;
|
|
772
|
+
/** Parsed `config.ownerFields` pairs — every declared "Entity.field" owner
|
|
773
|
+
* column, split once so `seed()` and `linkRelationFields()` don't each
|
|
774
|
+
* re-parse the same strings. */
|
|
775
|
+
private parsedOwnerFields;
|
|
725
776
|
/**
|
|
726
777
|
* Generate a single mock item. Field values come from the canonical policy in
|
|
727
778
|
* `@almadar/core/mock`; this method owns only the id and timestamp stamping.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EffectExecutor } from './chunk-
|
|
2
|
-
export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes } from './chunk-
|
|
1
|
+
import { EffectExecutor } from './chunk-IFSFX43J.js';
|
|
2
|
+
export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes } from './chunk-IFSFX43J.js';
|
|
3
3
|
export { cronMatches, cronMinuteKey, isValidCronExpression, parseCron, parseCronField } from './chunk-OU3ITB5S.js';
|
|
4
4
|
import { createContextFromBindings, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
|
|
5
5
|
export { CALLSITE_PAYLOAD_PREFIX, applyRowAccess, checkMutationAccess, containsBindings, createContextFromBindings, createMinimalContext, deferEntityBindings, extractBindings, interpolateProps, interpolateValue, resolveCallSitePayloadCaptures } from './chunk-ZJ62H3ES.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almadar/runtime",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.64.0",
|
|
4
4
|
"description": "Interpreted runtime for Almadar orbital applications (OrbitalServerRuntime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@almadar/core": "^10.
|
|
60
|
+
"@almadar/core": "^10.75.0",
|
|
61
61
|
"@almadar/evaluator": "^2.42.0",
|
|
62
62
|
"@almadar/logger": "^1.11.0",
|
|
63
63
|
"@almadar/server": "^2.38.0",
|
|
64
|
-
"@almadar/std": "^16.
|
|
64
|
+
"@almadar/std": "^16.194.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"express": "^5.0.0"
|