@cosmicdrift/kumiko-framework 0.20.0 → 0.21.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/LICENSE CHANGED
@@ -21,7 +21,7 @@ managed offering of the Licensed Work.
21
21
  This restriction does not apply to the Licensor, any entity controlled by,
22
22
  controlling, or under common control with the Licensor ("Affiliates"), or
23
23
  contractors acting on their behalf. The Licensor remains free to use the
24
- Licensed Work for any purpose, including for the operation of kumiko.so.
24
+ Licensed Work for any purpose, including for the operation of kumiko.rocks.
25
25
 
26
26
  Change Date: 2030-05-05
27
27
  Change License: Apache License, Version 2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.20.0",
3
+ "version": "0.21.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>",
@@ -12,7 +12,7 @@
12
12
  "bugs": {
13
13
  "url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
14
14
  },
15
- "homepage": "https://kumiko.so",
15
+ "homepage": "https://kumiko.rocks",
16
16
  "keywords": [
17
17
  "framework",
18
18
  "multi-tenant",
@@ -4,7 +4,7 @@
4
4
  // Plattform-Betrieb einsetzt. Wird oeffentlich exposed unter
5
5
  // /api/compliance/sub-processors (JSON, Sprint 1)
6
6
  // /api/compliance/sub-processors.rss (RSS, Sprint 1)
7
- // kumiko.so/subprocessors (HTML, Marketing-Repo)
7
+ // kumiko.rocks/subprocessors (HTML, Marketing-Repo)
8
8
  //
9
9
  // Tenant-Admins muessen ueber Add/Change/Remove informiert werden mit
10
10
  // Lead-Time aus dem Compliance-Profile (typisch 30d). Cron-Job kommt
@@ -614,7 +614,7 @@ export type TypedAppendEventArgs<TMap extends object, K extends keyof TMap> = {
614
614
  // We pick the first option and force the wrong path to fail visibly.
615
615
  //
616
616
  // How this is wired in practice:
617
- // - Apps run `yarn kumiko codegen`, which writes `.kumiko/define.ts`
617
+ // - Apps run `bun kumiko codegen`, which writes `.kumiko/define.ts`
618
618
  // with locally-bound `defineWriteHandler<TName, TSchema, TData,
619
619
  // KumikoEventTypeMap>(...)` wrappers. Handlers inside those wrappers
620
620
  // get a strict ctx.appendEvent.
@@ -30,22 +30,22 @@ describe("KumikoError: abstract base", () => {
30
30
  describe("docsUrl getter — Self-Service-Link", () => {
31
31
  test("uses details.reason when set (NotFoundError sets entity-specific reason)", () => {
32
32
  const err = new NotFoundError("order", 42);
33
- expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/order_not_found");
33
+ expect(err.docsUrl).toBe("https://docs.kumiko.rocks/errors/order_not_found");
34
34
  });
35
35
 
36
36
  test("uses details.reason when explicitly set (ConflictError-style)", () => {
37
37
  const err = new ConflictError({ details: { reason: "stale_state" } });
38
- expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/stale_state");
38
+ expect(err.docsUrl).toBe("https://docs.kumiko.rocks/errors/stale_state");
39
39
  });
40
40
 
41
41
  test("falls back to code when details has no reason field", () => {
42
42
  const err = new ConflictError({ details: { foo: "bar" } });
43
- expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/conflict");
43
+ expect(err.docsUrl).toBe("https://docs.kumiko.rocks/errors/conflict");
44
44
  });
45
45
 
46
46
  test("falls back to code when details is undefined", () => {
47
47
  const err = new ConflictError();
48
- expect(err.docsUrl).toBe("https://docs.kumiko.so/errors/conflict");
48
+ expect(err.docsUrl).toBe("https://docs.kumiko.rocks/errors/conflict");
49
49
  });
50
50
 
51
51
  test("respects KUMIKO_DOCS_URL env override (Self-Hosted-Kunden)", () => {
@@ -63,7 +63,7 @@ describe("KumikoError: abstract base", () => {
63
63
  test("serializeError exposes docsUrl in the wire response", () => {
64
64
  const err = new ConflictError({ details: { reason: "stale_state" } });
65
65
  const body = serializeError(err);
66
- expect(body.error.docsUrl).toBe("https://docs.kumiko.so/errors/stale_state");
66
+ expect(body.error.docsUrl).toBe("https://docs.kumiko.rocks/errors/stale_state");
67
67
  });
68
68
  });
69
69
  });
@@ -22,7 +22,7 @@ export type ErrorCtorInput = {
22
22
  // Default-Doku-URL für Self-Service-Errors. Kann via env-var
23
23
  // `KUMIKO_DOCS_URL` überschrieben werden — z.B. für Self-Hosted-Kunden
24
24
  // die ihre eigene Doku-Instanz hosten.
25
- const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.so";
25
+ const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.rocks";
26
26
 
27
27
  function docsBaseUrl(): string {
28
28
  return process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
@@ -1,6 +1,4 @@
1
1
  // Drizzle-free gate (kumiko/migrations system) — the canonical boot-gate.
2
- // `SchemaDriftError` is re-exported from here; the legacy drizzle gate above
3
- // keeps its own internal error until Phase 3 removes schema-drift.ts.
4
2
  export {
5
3
  assertKumikoSchemaCurrent,
6
4
  type ChecksumMismatch,
@@ -9,30 +7,5 @@ export {
9
7
  type KumikoDriftReport,
10
8
  SchemaDriftError,
11
9
  } from "./kumiko-drift";
12
- export {
13
- buildProjectionTableIndex,
14
- type ChangedTable,
15
- compareSnapshots,
16
- detectProjectionsToRebuild,
17
- latestMigrationTag,
18
- projectionsFromChanges,
19
- } from "./projection-detection";
20
- export { type RebuildMarker, readRebuildMarker, writeRebuildMarker } from "./rebuild-marker";
21
- export {
22
- type AppliedMigration,
23
- assertSchemaCurrent,
24
- type ColumnIssue,
25
- type ColumnSpec,
26
- type DriftReport,
27
- detectDrift,
28
- formatDriftReport,
29
- type Journal,
30
- type JournalEntry,
31
- loadAppliedMigrations,
32
- loadJournal,
33
- loadLatestSnapshot,
34
- loadPreviousSnapshot,
35
- loadSnapshot,
36
- type Snapshot,
37
- type SnapshotTable,
38
- } from "./schema-drift";
10
+ // tableName → projection-name, für den app-seitigen Projection-Rebuild.
11
+ export { buildProjectionTableIndex } from "./projection-table-index";
@@ -0,0 +1,35 @@
1
+ // Index `tableName → projection-name` aus der Registry. Genutzt vom
2
+ // app-seitigen Projection-Rebuild (`kumiko schema apply` liest die
3
+ // rebuild-Marker → mappt Tabellen auf Projektionen → rebuildProjection).
4
+ //
5
+ // Drizzle-frei: der Tabellen-Name kommt aus dem kumiko-Symbol das
6
+ // buildEntityTable/buildEntityTableMeta an die Table-Definition hängt.
7
+
8
+ import type { Registry } from "../engine/types/feature";
9
+
10
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
11
+
12
+ function getTableName(table: unknown): string {
13
+ if (typeof table !== "object" || table === null) {
14
+ throw new Error("projection-table-index: table is not a table object");
15
+ }
16
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
17
+ if (typeof name !== "string") {
18
+ throw new Error("projection-table-index: table missing kumiko name symbol");
19
+ }
20
+ return name;
21
+ }
22
+
23
+ /** Index `tableName → projection-name` aus der Registry. Nur Projections mit
24
+ * table-Definition (single-stream + multi-stream-with-table) zählen.
25
+ * Side-effect-only MSPs (table omitted) haben keinen Rebuild-Sinn. */
26
+ export function buildProjectionTableIndex(registry: Registry): ReadonlyMap<string, string> {
27
+ const index = new Map<string, string>();
28
+ for (const [name, def] of registry.getAllProjections()) {
29
+ index.set(getTableName(def.table), name);
30
+ }
31
+ for (const [name, def] of registry.getAllMultiStreamProjections()) {
32
+ if (def.table) index.set(getTableName(def.table), name);
33
+ }
34
+ return index;
35
+ }
@@ -99,7 +99,7 @@ export async function rebuildMultiStreamProjection(
99
99
  }
100
100
  if (!msp.table) {
101
101
  throw new Error(
102
- `MultiStreamProjection "${mspName}" has no backing table — it is a pure side-effect consumer (webhooks, notifications, external sync). Rebuild would re-invoke those side-effects by replaying the log. For poison events use yarn kumiko consumer skip / restart; there is no analogous "rebuild" concept for side-effect sinks.`,
102
+ `MultiStreamProjection "${mspName}" has no backing table — it is a pure side-effect consumer (webhooks, notifications, external sync). Rebuild would re-invoke those side-effects by replaying the log. For poison events use bun kumiko consumer skip / restart; there is no analogous "rebuild" concept for side-effect sinks.`,
103
103
  );
104
104
  }
105
105
 
@@ -1,35 +0,0 @@
1
- import type { AnyDb } from "../query";
2
- import { asRawClient } from "../query";
3
-
4
- export type AppliedMigrationRow = {
5
- readonly hash: string;
6
- readonly created_at: bigint | number | null;
7
- };
8
-
9
- export type DbColumnInfoRow = {
10
- readonly column_name: string;
11
- readonly data_type: string;
12
- readonly is_nullable: "YES" | "NO";
13
- };
14
-
15
- /** tableRef must be `drizzle.__drizzle_migrations` or `public.__drizzle_migrations`. */
16
- export async function selectAppliedMigrations(
17
- db: AnyDb,
18
- tableRef: "drizzle.__drizzle_migrations" | "public.__drizzle_migrations",
19
- ): Promise<readonly AppliedMigrationRow[]> {
20
- return (await asRawClient(db).unsafe(
21
- `SELECT hash, created_at FROM ${tableRef} ORDER BY id`,
22
- )) as readonly AppliedMigrationRow[];
23
- }
24
-
25
- export async function selectPublicTableColumns(
26
- db: AnyDb,
27
- tableName: string,
28
- ): Promise<readonly DbColumnInfoRow[]> {
29
- return (await asRawClient(db).unsafe(
30
- `SELECT column_name, data_type, is_nullable
31
- FROM information_schema.columns
32
- WHERE table_schema = 'public' AND table_name = $1`,
33
- [tableName],
34
- )) as readonly DbColumnInfoRow[];
35
- }
@@ -1,150 +0,0 @@
1
- // Unit-Tests für compareSnapshots — der Diff-Algorithmus zwischen zwei
2
- // Drizzle-Snapshots. Production-Behavior: bei Schema-Drift einer
3
- // Projection-Tabelle muss der Detector die richtigen Tabellen-Namen
4
- // melden, damit migrate apply den richtigen Rebuild triggert.
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import { compareSnapshots } from "../projection-detection";
8
- import type { Snapshot, SnapshotTable } from "../schema-drift";
9
-
10
- function snapshot(tables: Record<string, Partial<SnapshotTable>>): Snapshot {
11
- const out: Record<string, SnapshotTable> = {};
12
- for (const [key, partial] of Object.entries(tables)) {
13
- out[key] = {
14
- schema: partial.schema ?? "",
15
- name: partial.name ?? key.replace(/^public\./, ""),
16
- columns: partial.columns ?? {},
17
- };
18
- }
19
- return { tables: out };
20
- }
21
-
22
- const userTable: SnapshotTable = {
23
- schema: "",
24
- name: "users",
25
- columns: {
26
- id: { name: "id", type: "uuid", primaryKey: true, notNull: true },
27
- email: { name: "email", type: "text", notNull: true },
28
- },
29
- };
30
-
31
- describe("compareSnapshots", () => {
32
- test("prev=null → all current tables marked as added", () => {
33
- const current = snapshot({ "public.users": userTable });
34
- const changes = compareSnapshots(null, current);
35
- expect(changes).toHaveLength(1);
36
- expect(changes[0]).toMatchObject({ tableName: "users", kind: "added" });
37
- });
38
-
39
- test("identical snapshots → no changes", () => {
40
- const s = snapshot({ "public.users": userTable });
41
- expect(compareSnapshots(s, s)).toHaveLength(0);
42
- });
43
-
44
- test("table appears in current → kind=added", () => {
45
- const prev = snapshot({});
46
- const current = snapshot({ "public.users": userTable });
47
- const changes = compareSnapshots(prev, current);
48
- expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "added" }]);
49
- });
50
-
51
- test("table missing in current → kind=removed", () => {
52
- const prev = snapshot({ "public.users": userTable });
53
- const current = snapshot({});
54
- const changes = compareSnapshots(prev, current);
55
- expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "removed" }]);
56
- });
57
-
58
- test("column added → kind=modified", () => {
59
- const prev = snapshot({ "public.users": userTable });
60
- const current = snapshot({
61
- "public.users": {
62
- ...userTable,
63
- columns: { ...userTable.columns, name: { name: "name", type: "text" } },
64
- },
65
- });
66
- const changes = compareSnapshots(prev, current);
67
- expect(changes).toEqual([{ fullName: "users", tableName: "users", kind: "modified" }]);
68
- });
69
-
70
- test("column type changed → kind=modified", () => {
71
- const prev = snapshot({ "public.users": userTable });
72
- const current = snapshot({
73
- "public.users": {
74
- ...userTable,
75
- columns: {
76
- ...userTable.columns,
77
- email: { name: "email", type: "varchar(255)", notNull: true },
78
- },
79
- },
80
- });
81
- expect(compareSnapshots(prev, current)).toEqual([
82
- { fullName: "users", tableName: "users", kind: "modified" },
83
- ]);
84
- });
85
-
86
- test("notNull flipped → kind=modified", () => {
87
- const prev = snapshot({ "public.users": userTable });
88
- const current = snapshot({
89
- "public.users": {
90
- ...userTable,
91
- columns: { ...userTable.columns, email: { name: "email", type: "text", notNull: false } },
92
- },
93
- });
94
- expect(compareSnapshots(prev, current)).toEqual([
95
- { fullName: "users", tableName: "users", kind: "modified" },
96
- ]);
97
- });
98
-
99
- test("default value changed → kind=modified", () => {
100
- const prev = snapshot({
101
- "public.users": {
102
- ...userTable,
103
- columns: {
104
- ...userTable.columns,
105
- status: { name: "status", type: "text", default: "'active'" },
106
- },
107
- },
108
- });
109
- const current = snapshot({
110
- "public.users": {
111
- ...userTable,
112
- columns: {
113
- ...userTable.columns,
114
- status: { name: "status", type: "text", default: "'pending'" },
115
- },
116
- },
117
- });
118
- expect(compareSnapshots(prev, current)).toEqual([
119
- { fullName: "users", tableName: "users", kind: "modified" },
120
- ]);
121
- });
122
-
123
- test("schema-prefix in fullName when set", () => {
124
- const prev = snapshot({});
125
- const current = snapshot({
126
- "auth.users": { ...userTable, schema: "auth" },
127
- });
128
- const changes = compareSnapshots(prev, current);
129
- expect(changes[0]?.fullName).toBe("auth.users");
130
- });
131
-
132
- test("multiple changes preserved with stable kind classification", () => {
133
- const tableA: SnapshotTable = { ...userTable, name: "a" };
134
- const tableB: SnapshotTable = { ...userTable, name: "b" };
135
- const tableC: SnapshotTable = { ...userTable, name: "c" };
136
- const prev = snapshot({
137
- "public.a": tableA,
138
- "public.b": tableB,
139
- });
140
- const current = snapshot({
141
- "public.a": tableA, // unchanged
142
- "public.c": tableC, // added
143
- // b removed
144
- });
145
- const changes = compareSnapshots(prev, current);
146
- expect(changes).toHaveLength(2);
147
- expect(changes.find((c) => c.tableName === "c")?.kind).toBe("added");
148
- expect(changes.find((c) => c.tableName === "b")?.kind).toBe("removed");
149
- });
150
- });