@cosmicdrift/kumiko-bundled-features 0.215.4 → 0.215.6
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-bundled-features",
|
|
3
|
-
"version": "0.215.
|
|
3
|
+
"version": "0.215.6",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -126,12 +126,12 @@
|
|
|
126
126
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
127
127
|
},
|
|
128
128
|
"dependencies": {
|
|
129
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.215.
|
|
130
|
-
"@cosmicdrift/kumiko-framework": "0.215.
|
|
131
|
-
"@cosmicdrift/kumiko-headless": "0.215.
|
|
132
|
-
"@cosmicdrift/kumiko-renderer": "0.215.
|
|
133
|
-
"@cosmicdrift/kumiko-renderer-web": "0.215.
|
|
134
|
-
"@cosmicdrift/kumiko-types": "0.215.
|
|
129
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.215.6",
|
|
130
|
+
"@cosmicdrift/kumiko-framework": "0.215.6",
|
|
131
|
+
"@cosmicdrift/kumiko-headless": "0.215.6",
|
|
132
|
+
"@cosmicdrift/kumiko-renderer": "0.215.6",
|
|
133
|
+
"@cosmicdrift/kumiko-renderer-web": "0.215.6",
|
|
134
|
+
"@cosmicdrift/kumiko-types": "0.215.6",
|
|
135
135
|
"@mollie/api-client": "^4.5.0",
|
|
136
136
|
"@node-rs/argon2": "^2.0.2",
|
|
137
137
|
"@types/mailparser": "^3.4.6",
|
|
@@ -160,6 +160,6 @@
|
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@testing-library/user-event": "^14.6.1",
|
|
162
162
|
"@types/qrcode": "^1.5.5",
|
|
163
|
-
"@cosmicdrift/kumiko-locale-de": "0.215.
|
|
163
|
+
"@cosmicdrift/kumiko-locale-de": "0.215.6"
|
|
164
164
|
}
|
|
165
165
|
}
|
|
@@ -266,7 +266,9 @@ export function createJobRunLogger(opts: JobRunLoggerOptions): JobRunLoggerCallb
|
|
|
266
266
|
// worker). The row already has started_at from onJobStart.
|
|
267
267
|
const row = await fetchOne<{ startedAt: Temporal.Instant }>(db, jobRunsTable, { id: runId });
|
|
268
268
|
const now = Temporal.Now.instant();
|
|
269
|
-
const duration = row
|
|
269
|
+
const duration = row
|
|
270
|
+
? Math.round(Number(now.since(row.startedAt).total({ unit: "millisecond" })))
|
|
271
|
+
: 0;
|
|
270
272
|
const payload = runFailedSchema.parse({
|
|
271
273
|
duration,
|
|
272
274
|
finishedAt: now.toString(),
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type DbConnection,
|
|
4
|
+
deleteMany,
|
|
5
|
+
fetchOne,
|
|
6
|
+
selectMany,
|
|
7
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
3
8
|
import { SYSTEM_TENANT_ID, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
9
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
10
|
import {
|
|
@@ -77,6 +82,32 @@ describe("seedLegalContentFromJson", () => {
|
|
|
77
82
|
expect(row.modifiedById).toBe(SYSTEM_USER_ID);
|
|
78
83
|
});
|
|
79
84
|
|
|
85
|
+
test("orphan projection (row without events) is recreated on ifExists:update", async () => {
|
|
86
|
+
await seedLegalContentFromJson(db, [
|
|
87
|
+
{ slug: "orphan-privacy", locale: "de", title: "DS", content: "old" },
|
|
88
|
+
]);
|
|
89
|
+
const before = await read("orphan-privacy", "de");
|
|
90
|
+
expect(before).not.toBeNull();
|
|
91
|
+
|
|
92
|
+
// Same desync as prod: projection left behind, event stream empty.
|
|
93
|
+
await deleteMany(db, eventsTable, { aggregateId: String(before!.id) });
|
|
94
|
+
expect(await read("orphan-privacy", "de")).not.toBeNull();
|
|
95
|
+
|
|
96
|
+
await seedLegalContentFromJson(db, [
|
|
97
|
+
{
|
|
98
|
+
slug: "orphan-privacy",
|
|
99
|
+
locale: "de",
|
|
100
|
+
title: "DS",
|
|
101
|
+
content: "new from template",
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
const after = await read("orphan-privacy", "de");
|
|
105
|
+
expect(after).toMatchObject({ content: "new from template", title: "DS" });
|
|
106
|
+
const events = await selectMany(db, eventsTable, { aggregateId: String(after!.id) });
|
|
107
|
+
expect(events.length).toBeGreaterThan(0);
|
|
108
|
+
expect(events.some((e) => e.type === "template-resource.created")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
80
111
|
test("identical re-seed is a no-op (no update event, version/modifiedAt stable)", async () => {
|
|
81
112
|
const blocks: LegalContentBlock[] = [
|
|
82
113
|
{ slug: "terms", locale: "de", title: "AGB", content: "stable" },
|
|
@@ -10,18 +10,43 @@
|
|
|
10
10
|
// bewusst nicht TestUsers (Prod-Seeds ≠ Test-Utilities).
|
|
11
11
|
|
|
12
12
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
createTenantDb,
|
|
15
|
+
type DbConnection,
|
|
16
|
+
deleteMany,
|
|
17
|
+
type EntityTableMeta,
|
|
18
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
14
19
|
import {
|
|
15
20
|
createSystemUser,
|
|
16
21
|
type SessionUser,
|
|
17
22
|
SYSTEM_TENANT_ID,
|
|
18
23
|
type TenantId,
|
|
19
24
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { getStreamVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
20
26
|
import { runEventStoreSeed, type SeedIfExists } from "@cosmicdrift/kumiko-framework/seeding";
|
|
21
27
|
import { type ContentFormat, TEXT_BLOCK_KIND, type UpsertKind } from "./constants";
|
|
22
28
|
import { executor } from "./handlers/shared";
|
|
23
29
|
import { type TemplateResourceRow, templateResourcesTable } from "./table";
|
|
24
30
|
|
|
31
|
+
/** Projection row without events — update() would 409 (expectedVersion from
|
|
32
|
+
* projection, stream currentVersion=0). Drop the orphan and treat as create
|
|
33
|
+
* so boot seeds (legal ifExists:update) survive desync. Stream version is the
|
|
34
|
+
* optimistic-lock source of truth when events exist. */
|
|
35
|
+
async function resolveExistingForEventStoreSeed(
|
|
36
|
+
db: DbConnection,
|
|
37
|
+
existingRow: { readonly id: string | number; readonly version: number } | null,
|
|
38
|
+
tenantId: TenantId,
|
|
39
|
+
): Promise<{ id: string; version: number } | null> {
|
|
40
|
+
if (existingRow == null) return null;
|
|
41
|
+
const id = String(existingRow.id);
|
|
42
|
+
const streamVersion = await getStreamVersion(db, id, tenantId);
|
|
43
|
+
if (streamVersion === 0) {
|
|
44
|
+
await deleteMany(db, templateResourcesTable as EntityTableMeta, { id });
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { id, version: streamVersion };
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
export type SeedSystemTemplateOptions = {
|
|
26
51
|
readonly slug: string;
|
|
27
52
|
readonly kind: UpsertKind;
|
|
@@ -43,12 +68,17 @@ export async function seedSystemTemplate(
|
|
|
43
68
|
const by = opts.by ?? createSystemUser(tenantId);
|
|
44
69
|
const tdb = createTenantDb(db, tenantId, "system");
|
|
45
70
|
|
|
46
|
-
const
|
|
71
|
+
const existingRow = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
47
72
|
tenantId,
|
|
48
73
|
slug: opts.slug,
|
|
49
74
|
kind: opts.kind,
|
|
50
75
|
locale: opts.locale,
|
|
51
|
-
})
|
|
76
|
+
});
|
|
77
|
+
const existing = await resolveExistingForEventStoreSeed(
|
|
78
|
+
db,
|
|
79
|
+
existingRow ? { id: existingRow.id, version: existingRow.version } : null,
|
|
80
|
+
tenantId,
|
|
81
|
+
);
|
|
52
82
|
|
|
53
83
|
const variableSchema = JSON.stringify(opts.variableSchema ?? {});
|
|
54
84
|
const linkedResources = JSON.stringify(opts.linkedResources ?? {});
|
|
@@ -127,9 +157,11 @@ export async function seedTextBlock(
|
|
|
127
157
|
kind: TEXT_BLOCK_KIND,
|
|
128
158
|
locale: opts.locale,
|
|
129
159
|
});
|
|
130
|
-
const existing =
|
|
131
|
-
|
|
132
|
-
: null
|
|
160
|
+
const existing = await resolveExistingForEventStoreSeed(
|
|
161
|
+
db,
|
|
162
|
+
existingRow ? { id: existingRow.id, version: existingRow.version } : null,
|
|
163
|
+
opts.tenantId,
|
|
164
|
+
);
|
|
133
165
|
|
|
134
166
|
const fields = {
|
|
135
167
|
slug: opts.slug,
|
|
@@ -519,7 +519,7 @@ async function runInSubTransaction(
|
|
|
519
519
|
begin?: (f: (tx: DbRunner) => Promise<void>) => Promise<void>;
|
|
520
520
|
savepoint?: (f: (tx: DbRunner) => Promise<void>) => Promise<void>;
|
|
521
521
|
};
|
|
522
|
-
// savepoint-FIRST — empirisch (Bun 1.
|
|
522
|
+
// savepoint-FIRST — empirisch (Bun 1.4.0) sind die Flächen NICHT
|
|
523
523
|
// mutually exclusive: eine TransactionSql exposed begin UND savepoint,
|
|
524
524
|
// nur die Top-Level-Connection hat ausschließlich begin. begin-first
|
|
525
525
|
// wählte im Tx-Fall das nested BEGIN (Prod-Incident-Klasse, s. Header);
|