@cosmicdrift/kumiko-framework 0.41.0 → 0.41.1
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.41.
|
|
3
|
+
"version": "0.41.1",
|
|
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>",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { asRawClient } from "../../db/query";
|
|
3
3
|
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
4
|
-
import { createEventsTable } from "../../event-store";
|
|
4
|
+
import { append, createEventsTable } from "../../event-store";
|
|
5
5
|
import {
|
|
6
6
|
createTestDb,
|
|
7
7
|
type TestDb,
|
|
@@ -246,3 +246,42 @@ describe("event-store-executor — sensitive fields", () => {
|
|
|
246
246
|
expect(event.payload.previous.apiToken).toBeUndefined();
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
describe("event-store-executor — detail liefert die Stream-Version", () => {
|
|
251
|
+
const crud = createEventStoreExecutor(table, entity, { entityName: "esExecUser" });
|
|
252
|
+
|
|
253
|
+
// Lifecycle-Writes (ctx.appendEvent) bumpen den Stream, ohne row.version
|
|
254
|
+
// anzufassen — gäbe detail die stale Row-Version heraus, liefe jedes
|
|
255
|
+
// darauf gebaute CRUD-Update (entityEdit nutzt detail.version als
|
|
256
|
+
// optimistic-lock-Basis) in ein garantiertes version_conflict.
|
|
257
|
+
// Prod-Repro: incident:open appended das Eröffnungs-Update → Stream v2,
|
|
258
|
+
// Row v1 → incident-edit konnte nie speichern.
|
|
259
|
+
test("nach ctx.appendEvent-artigem Stream-Bump: detail.version == Stream, Update damit erfolgreich", async () => {
|
|
260
|
+
const created = await crud.create({ email: "stream@test.de" }, adminUser, tdb);
|
|
261
|
+
expect(created.isSuccess).toBe(true);
|
|
262
|
+
if (!created.isSuccess) return;
|
|
263
|
+
const id = created.data.id;
|
|
264
|
+
|
|
265
|
+
// Hand-emittiertes Event auf demselben Aggregat (wie incident:post-update).
|
|
266
|
+
await append(testDb.db, {
|
|
267
|
+
aggregateId: String(id),
|
|
268
|
+
aggregateType: "esExecUser",
|
|
269
|
+
tenantId: adminUser.tenantId,
|
|
270
|
+
expectedVersion: 1,
|
|
271
|
+
type: "esExecUser.lifecycle-bumped",
|
|
272
|
+
payload: { note: "stream moved past the row" },
|
|
273
|
+
metadata: { userId: String(adminUser.id) },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const detail = await crud.detail({ id }, adminUser, tdb);
|
|
277
|
+
expect(detail).not.toBeNull();
|
|
278
|
+
expect(detail?.["version"]).toBe(2);
|
|
279
|
+
|
|
280
|
+
const updated = await crud.update(
|
|
281
|
+
{ id, version: 2, changes: { firstName: "After" } },
|
|
282
|
+
adminUser,
|
|
283
|
+
tdb,
|
|
284
|
+
);
|
|
285
|
+
expect(updated.isSuccess).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -969,6 +969,19 @@ export function createEventStoreExecutor(
|
|
|
969
969
|
|
|
970
970
|
const idWhere = idFilter(payload.id);
|
|
971
971
|
|
|
972
|
+
// Stream-version authoritative (same policy as update/Block 0):
|
|
973
|
+
// ctx.appendEvent (lifecycle-writes like incident:post-update) bumps
|
|
974
|
+
// the stream WITHOUT touching row.version — a detail-read that hands
|
|
975
|
+
// out the stale row.version dooms the next CRUD update built on it
|
|
976
|
+
// (entityEdit loads detail.version as its optimistic-lock base) to a
|
|
977
|
+
// guaranteed version_conflict.
|
|
978
|
+
const withStreamVersion = async (
|
|
979
|
+
row: Record<string, unknown>,
|
|
980
|
+
): Promise<Record<string, unknown>> => {
|
|
981
|
+
const streamVersion = await getStreamVersion(db.raw, String(payload.id), user.tenantId);
|
|
982
|
+
return streamVersion > 0 ? { ...row, version: streamVersion } : row;
|
|
983
|
+
};
|
|
984
|
+
|
|
972
985
|
if (entityCache && entityName) {
|
|
973
986
|
const cached = await entityCache.get(user.tenantId, entityName, payload.id);
|
|
974
987
|
if (cached) {
|
|
@@ -978,7 +991,7 @@ export function createEventStoreExecutor(
|
|
|
978
991
|
const checkRows = await loadWithOwnership(db, idWhere, ownership);
|
|
979
992
|
if (checkRows.length === 0) return null;
|
|
980
993
|
}
|
|
981
|
-
return cached;
|
|
994
|
+
return withStreamVersion(cached);
|
|
982
995
|
}
|
|
983
996
|
}
|
|
984
997
|
|
|
@@ -993,7 +1006,7 @@ export function createEventStoreExecutor(
|
|
|
993
1006
|
await entityCache.set(user.tenantId, entityName, payload.id, coerced);
|
|
994
1007
|
}
|
|
995
1008
|
|
|
996
|
-
return coerced;
|
|
1009
|
+
return withStreamVersion(coerced);
|
|
997
1010
|
},
|
|
998
1011
|
};
|
|
999
1012
|
}
|