@cosmicdrift/kumiko-framework 0.56.1 → 0.57.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
2
2
|
import { asRawClient } from "../../db/query";
|
|
3
3
|
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
4
4
|
import { append, createEventsTable } from "../../event-store";
|
|
5
|
+
import type { EntityCache } from "../../pipeline/entity-cache";
|
|
5
6
|
import {
|
|
6
7
|
createTestDb,
|
|
7
8
|
type TestDb,
|
|
@@ -285,3 +286,48 @@ describe("event-store-executor — detail liefert die Stream-Version", () => {
|
|
|
285
286
|
expect(updated.isSuccess).toBe(true);
|
|
286
287
|
});
|
|
287
288
|
});
|
|
289
|
+
|
|
290
|
+
describe("event-store-executor — entity cache read-through", () => {
|
|
291
|
+
// In-memory stand-in for EntityCache — no Redis needed.
|
|
292
|
+
const store = new Map<string, Record<string, unknown>>();
|
|
293
|
+
const entityCache: EntityCache = {
|
|
294
|
+
get: async (tenantId, name, id) => store.get(`${tenantId}:${name}:${id}`) ?? null,
|
|
295
|
+
mget: async () => new Map(),
|
|
296
|
+
set: async (tenantId, name, id, data) => {
|
|
297
|
+
store.set(`${tenantId}:${name}:${id}`, data);
|
|
298
|
+
},
|
|
299
|
+
mset: async (tenantId, name, entries) => {
|
|
300
|
+
for (const { id, data } of entries) store.set(`${tenantId}:${name}:${id}`, data);
|
|
301
|
+
},
|
|
302
|
+
del: async (tenantId, name, id) => {
|
|
303
|
+
store.delete(`${tenantId}:${name}:${id}`);
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
const cachedCrud = createEventStoreExecutor(table, entity, {
|
|
307
|
+
entityName: "esExecUser",
|
|
308
|
+
entityCache,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
beforeEach(() => store.clear());
|
|
312
|
+
|
|
313
|
+
test("first detail populates cache; second detail is served from cache", async () => {
|
|
314
|
+
const created = await cachedCrud.create({ email: "cache@test.de" }, adminUser, tdb);
|
|
315
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
316
|
+
const id = created.data.id;
|
|
317
|
+
const storeKey = `${adminUser.tenantId}:esExecUser:${id}`;
|
|
318
|
+
|
|
319
|
+
// create() calls del() — cache must be empty after create.
|
|
320
|
+
expect(store.has(storeKey)).toBe(false);
|
|
321
|
+
|
|
322
|
+
// First detail: miss → DB → populates cache.
|
|
323
|
+
const first = await cachedCrud.detail({ id }, adminUser, tdb);
|
|
324
|
+
expect(first).not.toBeNull();
|
|
325
|
+
expect(store.has(storeKey)).toBe(true);
|
|
326
|
+
|
|
327
|
+
// Poison the cache entry — proves the second detail reads from cache, not DB.
|
|
328
|
+
store.set(storeKey, { ...store.get(storeKey)!, email: "from-cache@test.de" });
|
|
329
|
+
|
|
330
|
+
const second = await cachedCrud.detail({ id }, adminUser, tdb);
|
|
331
|
+
expect(second?.["email"]).toBe("from-cache@test.de");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConfigScope } from "./constants";
|
|
2
|
+
import { SYSTEM_ROLE } from "./system-user";
|
|
2
3
|
import type {
|
|
3
4
|
ConfigBacking,
|
|
4
5
|
ConfigBounds,
|
|
@@ -27,6 +28,13 @@ export const access = {
|
|
|
27
28
|
authenticated: ["User", "Admin", "SystemAdmin"] as readonly string[], // @cast-boundary schema-walk
|
|
28
29
|
anonymous: ["anonymous"] as readonly string[], // @cast-boundary schema-walk
|
|
29
30
|
roles: (...roles: string[]): readonly string[] => roles,
|
|
31
|
+
// Tenant self-service roles PLUS the system actor — for tenant-scope keys a
|
|
32
|
+
// tenant edits itself (configEdit) but that provisioning/migration/jobs must
|
|
33
|
+
// also set via ctx.systemWriteAs (roles = [SYSTEM_ROLE]). Stays human-writable
|
|
34
|
+
// because checkWriteAccess only collapses to system-only when system is the
|
|
35
|
+
// SOLE writer (humanWriters empty). Composes any preset, so apps with custom
|
|
36
|
+
// roles get the same provisioning path (issue #396).
|
|
37
|
+
withSystem: (roles: readonly string[]): readonly string[] => [SYSTEM_ROLE, ...roles],
|
|
30
38
|
} as const;
|
|
31
39
|
|
|
32
40
|
// --- Config Key Options ---
|