@cosmicdrift/kumiko-framework 0.127.0 → 0.129.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.129.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>",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"zod": "^4.4.3"
|
|
190
190
|
},
|
|
191
191
|
"devDependencies": {
|
|
192
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
192
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.129.0",
|
|
193
193
|
"bun-types": "^1.3.13",
|
|
194
194
|
"pino-pretty": "^13.1.3"
|
|
195
195
|
},
|
|
@@ -10,8 +10,11 @@ import {
|
|
|
10
10
|
defineEntityUpdateHandler,
|
|
11
11
|
defineEntityWriteHandler,
|
|
12
12
|
defineProjectionQueryHandler,
|
|
13
|
+
type EntityCrudRegistrar,
|
|
14
|
+
registerEntityCrud,
|
|
13
15
|
} from "../entity-handlers";
|
|
14
16
|
import { createEntity, createTextField } from "../factories";
|
|
17
|
+
import type { QueryHandlerDef, WriteHandlerDef } from "../types";
|
|
15
18
|
|
|
16
19
|
const VALID_UUID = "00000000-0000-4000-8000-000000000001";
|
|
17
20
|
|
|
@@ -272,3 +275,80 @@ describe("Verb-specific entity-handler factories", () => {
|
|
|
272
275
|
expect(typeof legacyApi.handler).toBe("function");
|
|
273
276
|
});
|
|
274
277
|
});
|
|
278
|
+
|
|
279
|
+
function createCrudRegistrarMock(): {
|
|
280
|
+
readonly r: EntityCrudRegistrar;
|
|
281
|
+
readonly entities: Record<string, unknown>;
|
|
282
|
+
readonly writes: WriteHandlerDef[];
|
|
283
|
+
readonly queries: QueryHandlerDef[];
|
|
284
|
+
} {
|
|
285
|
+
const entities: Record<string, unknown> = {};
|
|
286
|
+
const writes: WriteHandlerDef[] = [];
|
|
287
|
+
const queries: QueryHandlerDef[] = [];
|
|
288
|
+
const r: EntityCrudRegistrar = {
|
|
289
|
+
entity(name, definition) {
|
|
290
|
+
entities[name] = definition;
|
|
291
|
+
},
|
|
292
|
+
writeHandler(def) {
|
|
293
|
+
writes.push(def);
|
|
294
|
+
},
|
|
295
|
+
queryHandler(def) {
|
|
296
|
+
queries.push(def);
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return { r, entities, writes, queries };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
describe("registerEntityCrud", () => {
|
|
303
|
+
test("registers entity + standard handlers (restore only when softDelete)", () => {
|
|
304
|
+
const { r, entities, writes, queries } = createCrudRegistrarMock();
|
|
305
|
+
registerEntityCrud(r, "note", noteEntitySoftDelete, {
|
|
306
|
+
write: { access: { roles: ["Admin"] } },
|
|
307
|
+
read: { access: { openToAll: true } },
|
|
308
|
+
});
|
|
309
|
+
expect(entities["note"]).toBe(noteEntitySoftDelete);
|
|
310
|
+
expect(writes.map((w) => w.name)).toEqual([
|
|
311
|
+
"note:create",
|
|
312
|
+
"note:update",
|
|
313
|
+
"note:delete",
|
|
314
|
+
"note:restore",
|
|
315
|
+
]);
|
|
316
|
+
expect(queries.map((q) => q.name)).toEqual(["note:list", "note:detail"]);
|
|
317
|
+
expect(writes[0]?.access).toEqual({ roles: ["Admin"] });
|
|
318
|
+
expect(queries[0]?.access).toEqual({ openToAll: true });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("verbs.delete: false skips delete handler", () => {
|
|
322
|
+
const { r, writes } = createCrudRegistrarMock();
|
|
323
|
+
registerEntityCrud(r, "note", noteEntity, {
|
|
324
|
+
verbs: { delete: false },
|
|
325
|
+
});
|
|
326
|
+
expect(writes.map((w) => w.name)).toEqual(["note:create", "note:update"]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("no options → handlers have no access (no implicit openToAll)", () => {
|
|
330
|
+
const { r, writes, queries } = createCrudRegistrarMock();
|
|
331
|
+
registerEntityCrud(r, "note", noteEntity);
|
|
332
|
+
for (const def of [...writes, ...queries]) {
|
|
333
|
+
expect(def.access).toBeUndefined();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("restore verb on entity without softDelete throws", () => {
|
|
338
|
+
const { r } = createCrudRegistrarMock();
|
|
339
|
+
expect(() => registerEntityCrud(r, "note", noteEntity, { verbs: { restore: true } })).toThrow(
|
|
340
|
+
/softDelete/,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("registerEntity: false skips r.entity when already registered", () => {
|
|
345
|
+
const { r, entities, writes } = createCrudRegistrarMock();
|
|
346
|
+
r.entity("note", noteEntity);
|
|
347
|
+
registerEntityCrud(r, "note", noteEntity, {
|
|
348
|
+
registerEntity: false,
|
|
349
|
+
verbs: { update: false, delete: false, restore: false, list: false, detail: false },
|
|
350
|
+
});
|
|
351
|
+
expect(Object.keys(entities)).toEqual(["note"]);
|
|
352
|
+
expect(writes.map((w) => w.name)).toEqual(["note:create"]);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -27,6 +27,9 @@ import type {
|
|
|
27
27
|
//
|
|
28
28
|
// Two API shapes — pick one per project, don't mix:
|
|
29
29
|
//
|
|
30
|
+
// PREFERRED — full standard CRUD set in one call:
|
|
31
|
+
// registerEntityCrud(r, "note", noteEntity, { write: { access }, read: { access } })
|
|
32
|
+
//
|
|
30
33
|
// PREFERRED — one function per verb, type-safe, no magic strings:
|
|
31
34
|
// r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }))
|
|
32
35
|
// r.writeHandler(defineEntityUpdateHandler("note", noteEntity, { access }))
|
|
@@ -434,3 +437,71 @@ export function defineProjectionQueryHandler(
|
|
|
434
437
|
...(options?.access && { access: options.access }),
|
|
435
438
|
};
|
|
436
439
|
}
|
|
440
|
+
|
|
441
|
+
type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
|
|
442
|
+
|
|
443
|
+
export type RegisterEntityCrudOptions = {
|
|
444
|
+
readonly write?: EntityHandlerOptions;
|
|
445
|
+
readonly read?: EntityQueryHandlerOptions;
|
|
446
|
+
readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
|
|
447
|
+
/** Default true. Set false when the entity was already registered (e.g. before r.relation). */
|
|
448
|
+
readonly registerEntity?: boolean;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/** Minimal registrar surface — keeps entity-handlers free of define-feature imports. */
|
|
452
|
+
export type EntityCrudRegistrar = {
|
|
453
|
+
entity(name: string, definition: EntityDefinition): unknown;
|
|
454
|
+
writeHandler(def: WriteHandlerDef): unknown;
|
|
455
|
+
queryHandler(def: QueryHandlerDef): unknown;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
function defaultCrudVerbs(entity: EntityDefinition): Record<EntityCrudVerb, boolean> {
|
|
459
|
+
return {
|
|
460
|
+
create: true,
|
|
461
|
+
update: true,
|
|
462
|
+
delete: true,
|
|
463
|
+
restore: entity.softDelete === true,
|
|
464
|
+
list: true,
|
|
465
|
+
detail: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Register standard entity CRUD handlers in one call. Access stays explicit — no openToAll default. */
|
|
470
|
+
export function registerEntityCrud(
|
|
471
|
+
r: EntityCrudRegistrar,
|
|
472
|
+
entityName: string,
|
|
473
|
+
entity: EntityDefinition,
|
|
474
|
+
options?: RegisterEntityCrudOptions,
|
|
475
|
+
): void {
|
|
476
|
+
const verbs = { ...defaultCrudVerbs(entity), ...options?.verbs };
|
|
477
|
+
if (verbs.restore && entity.softDelete !== true) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`registerEntityCrud("${entityName}"): restore requested but entity has no softDelete: true`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (options?.registerEntity !== false) {
|
|
484
|
+
r.entity(entityName, entity);
|
|
485
|
+
}
|
|
486
|
+
const writeOpts = options?.write;
|
|
487
|
+
const readOpts = options?.read;
|
|
488
|
+
|
|
489
|
+
if (verbs.create) {
|
|
490
|
+
r.writeHandler(defineEntityCreateHandler(entityName, entity, writeOpts));
|
|
491
|
+
}
|
|
492
|
+
if (verbs.update) {
|
|
493
|
+
r.writeHandler(defineEntityUpdateHandler(entityName, entity, writeOpts));
|
|
494
|
+
}
|
|
495
|
+
if (verbs.delete) {
|
|
496
|
+
r.writeHandler(defineEntityDeleteHandler(entityName, entity, writeOpts));
|
|
497
|
+
}
|
|
498
|
+
if (verbs.restore) {
|
|
499
|
+
r.writeHandler(defineEntityRestoreHandler(entityName, entity, writeOpts));
|
|
500
|
+
}
|
|
501
|
+
if (verbs.list) {
|
|
502
|
+
r.queryHandler(defineEntityListHandler(entityName, entity, readOpts));
|
|
503
|
+
}
|
|
504
|
+
if (verbs.detail) {
|
|
505
|
+
r.queryHandler(defineEntityDetailHandler(entityName, entity, readOpts));
|
|
506
|
+
}
|
|
507
|
+
}
|
package/src/engine/index.ts
CHANGED
|
@@ -66,6 +66,9 @@ export {
|
|
|
66
66
|
defineEntityUpdateHandler,
|
|
67
67
|
defineEntityWriteHandler,
|
|
68
68
|
defineProjectionQueryHandler,
|
|
69
|
+
type EntityCrudRegistrar,
|
|
70
|
+
type RegisterEntityCrudOptions,
|
|
71
|
+
registerEntityCrud,
|
|
69
72
|
} from "./entity-handlers";
|
|
70
73
|
export type { EmitCtx } from "./event-helpers";
|
|
71
74
|
export { emitEvent, typedPayload } from "./event-helpers";
|