@cosmicdrift/kumiko-bundled-features 0.105.0 → 0.105.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-bundled-features",
|
|
3
|
-
"version": "0.105.
|
|
3
|
+
"version": "0.105.1",
|
|
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>",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"./text-content/seeding": "./src/text-content/seeding.ts",
|
|
83
83
|
"./text-content/web": "./src/text-content/web/index.ts",
|
|
84
84
|
"./template-resolver": "./src/template-resolver/index.ts",
|
|
85
|
+
"./template-resolver/seeding": "./src/template-resolver/seeding.ts",
|
|
85
86
|
"./template-resolver/testing": "./src/template-resolver/testing.ts",
|
|
86
87
|
"./renderer-foundation": "./src/renderer-foundation/index.ts",
|
|
87
88
|
"./legal-pages": "./src/legal-pages/index.ts",
|
|
@@ -92,11 +93,11 @@
|
|
|
92
93
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
93
94
|
},
|
|
94
95
|
"dependencies": {
|
|
95
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.105.
|
|
96
|
-
"@cosmicdrift/kumiko-framework": "0.105.
|
|
97
|
-
"@cosmicdrift/kumiko-headless": "0.105.
|
|
98
|
-
"@cosmicdrift/kumiko-renderer": "0.105.
|
|
99
|
-
"@cosmicdrift/kumiko-renderer-web": "0.105.
|
|
96
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.105.1",
|
|
97
|
+
"@cosmicdrift/kumiko-framework": "0.105.1",
|
|
98
|
+
"@cosmicdrift/kumiko-headless": "0.105.1",
|
|
99
|
+
"@cosmicdrift/kumiko-renderer": "0.105.1",
|
|
100
|
+
"@cosmicdrift/kumiko-renderer-web": "0.105.1",
|
|
100
101
|
"@mollie/api-client": "^4.5.0",
|
|
101
102
|
"@node-rs/argon2": "^2.0.2",
|
|
102
103
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -55,6 +55,23 @@ async function someHandler(ctx) {
|
|
|
55
55
|
|
|
56
56
|
Wenn nichts gefunden → `TemplateNotFoundError`.
|
|
57
57
|
|
|
58
|
+
## Boot-Seeding (System-Templates)
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { seedSystemTemplate } from "@cosmicdrift/kumiko-bundled-features/template-resolver/seeding";
|
|
62
|
+
|
|
63
|
+
// runProdApp seeds — idempotent, default skip if slug exists
|
|
64
|
+
await seedSystemTemplate(db, {
|
|
65
|
+
slug: "welcome-email",
|
|
66
|
+
kind: "notification",
|
|
67
|
+
locale: "de",
|
|
68
|
+
content: JSON.stringify({ header: "Willkommen", sections: [{ text: "…" }] }),
|
|
69
|
+
contentFormat: "plain",
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`ifExists: "update"` für autoritative Re-Seeds (z.B. nach Template-Content-Release).
|
|
74
|
+
|
|
58
75
|
## Admin-Workflows (Write-Handlers + Queries)
|
|
59
76
|
|
|
60
77
|
| Handler | QN | Wer | Was |
|
|
@@ -66,7 +83,7 @@ Wenn nichts gefunden → `TemplateNotFoundError`.
|
|
|
66
83
|
| `TemplateResolverQueries.findById` | `template-resolver:query:find-by-id` | TenantAdmin + User (eigener Tenant + system-templates sichtbar) | Raw-Lookup für Edit-UI |
|
|
67
84
|
| `TemplateResolverQueries.list` | `template-resolver:query:list` | gleich | Filter nach kind/locale/status, optional includeSystem |
|
|
68
85
|
|
|
69
|
-
**SystemAdmin-Cross-Tenant für publish/archive/findById:**
|
|
86
|
+
**SystemAdmin-Cross-Tenant für publish/archive/findById:** deferred (Task 8) — `upsertTenant` hat `tenantIdOverride` bereits; publish/archive brauchen das erst wenn ein Sysadmin-Panel fremde Tenant-Templates kuratiert. `ctx.db` ist tenant-scoped, fremde IDs → NotFound ohne Override.
|
|
70
87
|
|
|
71
88
|
## Status-Lifecycle
|
|
72
89
|
|
|
@@ -109,3 +126,5 @@ The harness checks `TemplateNotFoundError` propagation, locale-fallback, and (wh
|
|
|
109
126
|
- Resource-URL-Substitution (signed-URL vs. data-URI) — Caller-Verantwortung je nach kind
|
|
110
127
|
- Visual Template-Editor — `designer`-Bundle (geplant)
|
|
111
128
|
- A/B-Testing — eigenes Bundle wenn Bedarf real
|
|
129
|
+
|
|
130
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
3
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import {
|
|
5
|
+
setupTestStack,
|
|
6
|
+
type TestStack,
|
|
7
|
+
testTenantId,
|
|
8
|
+
unsafeCreateEntityTable,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
10
|
+
import { createTemplateResolverApi } from "../api";
|
|
11
|
+
import { SYSTEM_TENANT_ID } from "../constants";
|
|
12
|
+
import { createTemplateResolverFeature } from "../feature";
|
|
13
|
+
import { seedSystemTemplate } from "../seeding";
|
|
14
|
+
import { type TemplateResourceRow, templateResourceEntity, templateResourcesTable } from "../table";
|
|
15
|
+
|
|
16
|
+
let stack: TestStack;
|
|
17
|
+
|
|
18
|
+
const TENANT_ID = testTenantId(710) as TenantId;
|
|
19
|
+
|
|
20
|
+
const feature = createTemplateResolverFeature();
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
stack = await setupTestStack({ features: [feature] });
|
|
24
|
+
await unsafeCreateEntityTable(stack.db, templateResourceEntity);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
await stack.cleanup();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("seedSystemTemplate", () => {
|
|
32
|
+
test("legt System-Template an, resolveTemplate findet es für beliebigen Tenant", async () => {
|
|
33
|
+
const slug = `welcome-${crypto.randomUUID()}`;
|
|
34
|
+
await seedSystemTemplate(stack.db, {
|
|
35
|
+
slug,
|
|
36
|
+
kind: "notification",
|
|
37
|
+
locale: "de",
|
|
38
|
+
content: JSON.stringify({ header: "Willkommen", sections: [{ text: "Hallo" }] }),
|
|
39
|
+
contentFormat: "plain",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const api = createTemplateResolverApi(stack.db);
|
|
43
|
+
const resolved = await api.resolveTemplate({
|
|
44
|
+
tenantId: TENANT_ID,
|
|
45
|
+
slug,
|
|
46
|
+
kind: "notification",
|
|
47
|
+
locale: "de",
|
|
48
|
+
});
|
|
49
|
+
expect(resolved.content).toContain("Willkommen");
|
|
50
|
+
expect(resolved.scope).toBe("system");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('ifExists="update" überschreibt content', async () => {
|
|
54
|
+
const slug = `incident-${crypto.randomUUID()}`;
|
|
55
|
+
await seedSystemTemplate(stack.db, {
|
|
56
|
+
slug,
|
|
57
|
+
kind: "notification",
|
|
58
|
+
locale: "en",
|
|
59
|
+
content: "v1",
|
|
60
|
+
contentFormat: "plain",
|
|
61
|
+
});
|
|
62
|
+
await seedSystemTemplate(stack.db, {
|
|
63
|
+
slug,
|
|
64
|
+
kind: "notification",
|
|
65
|
+
locale: "en",
|
|
66
|
+
content: "v2",
|
|
67
|
+
contentFormat: "plain",
|
|
68
|
+
ifExists: "update",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const row = await fetchOne<TemplateResourceRow>(stack.db, templateResourcesTable, {
|
|
72
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
73
|
+
slug,
|
|
74
|
+
kind: "notification",
|
|
75
|
+
locale: "en",
|
|
76
|
+
});
|
|
77
|
+
expect(row?.content).toBe("v2");
|
|
78
|
+
expect(row?.version).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("default skip: zweiter Boot-Call ohne update ändert nichts", async () => {
|
|
82
|
+
const slug = `status-${crypto.randomUUID()}`;
|
|
83
|
+
await seedSystemTemplate(stack.db, {
|
|
84
|
+
slug,
|
|
85
|
+
kind: "notification",
|
|
86
|
+
locale: "de",
|
|
87
|
+
content: "original",
|
|
88
|
+
contentFormat: "plain",
|
|
89
|
+
});
|
|
90
|
+
await seedSystemTemplate(stack.db, {
|
|
91
|
+
slug,
|
|
92
|
+
kind: "notification",
|
|
93
|
+
locale: "de",
|
|
94
|
+
content: "would-overwrite",
|
|
95
|
+
contentFormat: "plain",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const row = await fetchOne<TemplateResourceRow>(stack.db, templateResourcesTable, {
|
|
99
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
100
|
+
slug,
|
|
101
|
+
kind: "notification",
|
|
102
|
+
locale: "de",
|
|
103
|
+
});
|
|
104
|
+
expect(row?.content).toBe("original");
|
|
105
|
+
expect(row?.version).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Boot-/Seed-Helper für App-Authors. Schreibt System-Templates (tenantId=
|
|
2
|
+
// SYSTEM_TENANT_ID, scope=system, status=active) idempotent in die DB —
|
|
3
|
+
// gleicher Projection-Pfad wie upsertSystem-Handler, ohne Access-Check.
|
|
4
|
+
//
|
|
5
|
+
// **Wann nutzen?** runProdApp-seeds / TenantCreated-Hooks die Welcome-,
|
|
6
|
+
// Incident- oder Feature-Mail-Slugs installieren, damit renderer-simple
|
|
7
|
+
// (oder mail-html) sie per template-resolver auflösen kann.
|
|
8
|
+
//
|
|
9
|
+
// Default ifExists="skip". `createSystemUser(SYSTEM_TENANT_ID)` als Actor —
|
|
10
|
+
// bewusst nicht TestUsers (Prod-Seeds ≠ Test-Utilities).
|
|
11
|
+
|
|
12
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
|
+
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
14
|
+
import {
|
|
15
|
+
createSystemUser,
|
|
16
|
+
type SessionUser,
|
|
17
|
+
SYSTEM_TENANT_ID,
|
|
18
|
+
type TenantId,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import { runEventStoreSeed, type SeedIfExists } from "@cosmicdrift/kumiko-framework/seeding";
|
|
21
|
+
import type { ContentFormat, RenderKind } from "./constants";
|
|
22
|
+
import { executor } from "./handlers/shared";
|
|
23
|
+
import { type TemplateResourceRow, templateResourcesTable } from "./table";
|
|
24
|
+
|
|
25
|
+
export type SeedSystemTemplateOptions = {
|
|
26
|
+
readonly slug: string;
|
|
27
|
+
readonly kind: RenderKind;
|
|
28
|
+
readonly locale: string;
|
|
29
|
+
readonly content: string;
|
|
30
|
+
readonly contentFormat: ContentFormat;
|
|
31
|
+
readonly variableSchema?: Record<string, unknown>;
|
|
32
|
+
readonly linkedResources?: Record<string, string>;
|
|
33
|
+
readonly parentTemplateId?: string | null;
|
|
34
|
+
readonly by?: SessionUser;
|
|
35
|
+
readonly ifExists?: SeedIfExists;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function seedSystemTemplate(
|
|
39
|
+
db: DbConnection,
|
|
40
|
+
opts: SeedSystemTemplateOptions,
|
|
41
|
+
): Promise<{ id: string }> {
|
|
42
|
+
const tenantId = SYSTEM_TENANT_ID as TenantId;
|
|
43
|
+
const by = opts.by ?? createSystemUser(tenantId);
|
|
44
|
+
const tdb = createTenantDb(db, tenantId, "system");
|
|
45
|
+
|
|
46
|
+
const existing = (await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
47
|
+
tenantId,
|
|
48
|
+
slug: opts.slug,
|
|
49
|
+
kind: opts.kind,
|
|
50
|
+
locale: opts.locale,
|
|
51
|
+
})) as { id: string; version: number } | null;
|
|
52
|
+
|
|
53
|
+
const variableSchema = JSON.stringify(opts.variableSchema ?? {});
|
|
54
|
+
const linkedResources = JSON.stringify(opts.linkedResources ?? {});
|
|
55
|
+
const parentTemplateId = opts.parentTemplateId ?? null;
|
|
56
|
+
|
|
57
|
+
const rowFields = {
|
|
58
|
+
slug: opts.slug,
|
|
59
|
+
kind: opts.kind,
|
|
60
|
+
locale: opts.locale,
|
|
61
|
+
content: opts.content,
|
|
62
|
+
contentFormat: opts.contentFormat,
|
|
63
|
+
variableSchema,
|
|
64
|
+
linkedResources,
|
|
65
|
+
scope: "system" as const,
|
|
66
|
+
parentTemplateId,
|
|
67
|
+
status: "active" as const,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return runEventStoreSeed({
|
|
71
|
+
existing,
|
|
72
|
+
ifExists: opts.ifExists,
|
|
73
|
+
create: async () => {
|
|
74
|
+
const result = await executor.create({ ...rowFields, tenantId }, by, tdb);
|
|
75
|
+
if (!result.isSuccess) {
|
|
76
|
+
throw new Error(`seedSystemTemplate create failed: ${JSON.stringify(result)}`);
|
|
77
|
+
}
|
|
78
|
+
const data = result.data as Partial<TemplateResourceRow>;
|
|
79
|
+
if (data.id === undefined) {
|
|
80
|
+
throw new Error("seedSystemTemplate: executor.create did not return an id");
|
|
81
|
+
}
|
|
82
|
+
return { id: String(data.id) };
|
|
83
|
+
},
|
|
84
|
+
update: async (row) => {
|
|
85
|
+
const result = await executor.update(
|
|
86
|
+
{ id: row.id, version: row.version, changes: rowFields },
|
|
87
|
+
by,
|
|
88
|
+
tdb,
|
|
89
|
+
);
|
|
90
|
+
if (!result.isSuccess) {
|
|
91
|
+
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
92
|
+
}
|
|
93
|
+
return { id: String(row.id) };
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|