@cosmicdrift/kumiko-framework 0.174.1 → 0.176.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.174.1",
3
+ "version": "0.176.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>",
@@ -182,7 +182,7 @@
182
182
  "./package.json": "./package.json"
183
183
  },
184
184
  "dependencies": {
185
- "@cosmicdrift/kumiko-types": "0.174.1",
185
+ "@cosmicdrift/kumiko-types": "0.176.0",
186
186
  "bullmq": "^5.76.7",
187
187
  "bun-types": "^1.3.13",
188
188
  "hono": "^4.12.27",
@@ -198,7 +198,7 @@
198
198
  "zod": "^4.4.3"
199
199
  },
200
200
  "devDependencies": {
201
- "@cosmicdrift/kumiko-dispatcher-live": "0.174.1",
201
+ "@cosmicdrift/kumiko-dispatcher-live": "0.176.0",
202
202
  "bun-types": "^1.3.13",
203
203
  "pino-pretty": "^13.1.3"
204
204
  },
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { defineFeature } from "../../engine/define-feature";
3
+ import { createEntity, createTextField } from "../../engine/factories";
4
+ import { createRegistry } from "../../engine/registry";
5
+ import { extractTableName, type SchemaTable } from "../dialect";
6
+ import { entityTableFromRegistry } from "../entity-table-from-registry";
7
+ import { buildEntityTable } from "../table-builder";
8
+
9
+ function unitEntity() {
10
+ return createEntity({ table: "read_units", fields: { name: createTextField() } });
11
+ }
12
+
13
+ describe("entityTableFromRegistry", () => {
14
+ test("returns the table the registry booted, not one derived from the definition", () => {
15
+ const registry = createRegistry([
16
+ defineFeature("housing", (r) => {
17
+ r.entity("unit", unitEntity());
18
+ }),
19
+ ]);
20
+
21
+ const fromRegistry = entityTableFromRegistry(registry, "unit", unitEntity());
22
+ const projection = [...registry.getAllProjections().values()].find(
23
+ (p) => p.isImplicit && p.source === "unit",
24
+ );
25
+
26
+ expect(projection).toBeDefined();
27
+ expect(fromRegistry).toBe(projection?.table as SchemaTable);
28
+ });
29
+
30
+ // An entity nobody registered has no projection to read — the caller still
31
+ // gets a usable table instead of undefined, which is what made the helper
32
+ // worth having over a raw lookup.
33
+ test("falls back to deriving the table when no implicit projection exists", () => {
34
+ const registry = createRegistry([]);
35
+
36
+ const derived = entityTableFromRegistry(registry, "unit", unitEntity());
37
+
38
+ expect(extractTableName(derived)).toBe(
39
+ extractTableName(buildEntityTable("unit", unitEntity())),
40
+ );
41
+ });
42
+
43
+ // A projection for a different entity must not be handed back: both are
44
+ // implicit, and picking the first one would silently write into the wrong
45
+ // table.
46
+ test("ignores implicit projections of other entities", () => {
47
+ const registry = createRegistry([
48
+ defineFeature("housing", (r) => {
49
+ r.entity("unit", unitEntity());
50
+ }),
51
+ ]);
52
+
53
+ const other = createEntity({ table: "read_tenants", fields: { name: createTextField() } });
54
+ const derived = entityTableFromRegistry(registry, "tenant", other);
55
+
56
+ expect(extractTableName(derived)).toBe("read_tenants");
57
+ });
58
+ });
59
+
60
+ // Guards the cast in entity-table-from-registry.ts: the helper hands the
61
+ // projection's table straight to callers that pass it to selectMany/executors,
62
+ // so it has to carry the physical name those consumers read off it.
63
+ test("the registry table carries a physical name", () => {
64
+ const registry = createRegistry([
65
+ defineFeature("housing", (r) => {
66
+ r.entity("unit", unitEntity());
67
+ }),
68
+ ]);
69
+
70
+ expect(extractTableName(entityTableFromRegistry(registry, "unit", unitEntity()))).toBe(
71
+ "read_units",
72
+ );
73
+ });
@@ -0,0 +1,31 @@
1
+ // The table an entity actually lives in, as the booted registry sees it.
2
+ //
3
+ // `buildEntityTable` derives a table from the entity definition alone, which is
4
+ // right until something changed the projection after registration — an
5
+ // extendEntityProjection, a featureName prefix, a table override. The registry
6
+ // carries the result of all of that, so a caller that needs to read or write
7
+ // the same rows the pipeline writes has to ask the registry first and may only
8
+ // fall back to deriving.
9
+ //
10
+ // Callers are the ones holding a registry and an entity definition but no
11
+ // executor: seeds, jobs and consumers that talk to a feature's read model
12
+ // directly.
13
+
14
+ import type { Registry } from "../engine";
15
+ import type { SchemaTable } from "./dialect";
16
+ import { buildEntityTable } from "./table-builder";
17
+
18
+ export function entityTableFromRegistry(
19
+ registry: Registry,
20
+ entityName: string,
21
+ entity: Parameters<typeof buildEntityTable>[1],
22
+ ): SchemaTable {
23
+ for (const projection of registry.getAllProjections().values()) {
24
+ if (!projection.isImplicit) continue;
25
+ if (projection.source !== entityName) continue;
26
+ // @cast-boundary registry-projection — ProjectionDefinition.table is the
27
+ // untyped table object every projection registrar accepts.
28
+ return projection.table as SchemaTable;
29
+ }
30
+ return buildEntityTable(entityName, entity);
31
+ }
package/src/db/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  decryptEntityFieldValues,
50
50
  encryptEntityFieldValues,
51
51
  } from "./entity-field-encryption";
52
+ export { entityTableFromRegistry } from "./entity-table-from-registry";
52
53
  export type {
53
54
  BuildEntityTableMetaOptions,
54
55
  ColumnMeta,