@cosmicdrift/kumiko-bundled-features 0.4.0 → 0.5.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/CHANGELOG.md +78 -0
- package/package.json +5 -5
- package/src/auth-email-password/i18n.ts +8 -0
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +128 -1
- package/src/auth-email-password/web/login-screen.tsx +73 -8
- package/src/config/__tests__/cascade.integration.ts +419 -0
- package/src/config/__tests__/config.integration.ts +109 -2
- package/src/config/constants.ts +1 -0
- package/src/config/feature.ts +2 -0
- package/src/config/handlers/cascade.query.ts +70 -0
- package/src/config/handlers/values.query.ts +14 -4
- package/src/config/index.ts +17 -0
- package/src/config/resolver.ts +273 -1
- package/src/template-resolver/api.ts +21 -5
- package/src/template-resolver/handlers/list.query.ts +2 -0
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -1
- package/src/template-resolver/handlers/upsert-tenant.write.ts +8 -1
package/src/config/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { DbConnection, EncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { seedConfigValues } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { configValueEntity, configValuesTable } from "./table";
|
|
5
|
+
|
|
1
6
|
export {
|
|
2
7
|
CONFIG_FEATURE,
|
|
3
8
|
ConfigErrors,
|
|
@@ -13,3 +18,15 @@ export {
|
|
|
13
18
|
export type { AppConfigOverrides, ConfigResolver } from "./resolver";
|
|
14
19
|
export { createConfigResolver, validateAppOverrides } from "./resolver";
|
|
15
20
|
export { configValuesTable } from "./table";
|
|
21
|
+
|
|
22
|
+
// Boot helper for runDevApp / runProdApp: pulls every ConfigSeedDef from
|
|
23
|
+
// the registry and writes the matching system/tenant/user rows via the
|
|
24
|
+
// event-store executor. Idempotent across boots — see config-seeding.md.
|
|
25
|
+
export function seedAllConfigValues(
|
|
26
|
+
registry: Registry,
|
|
27
|
+
db: DbConnection,
|
|
28
|
+
encryption?: EncryptionProvider,
|
|
29
|
+
): Promise<{ created: number; skipped: number }> {
|
|
30
|
+
const seeds = registry.getAllConfigSeeds();
|
|
31
|
+
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, encryption);
|
|
32
|
+
}
|
package/src/config/resolver.ts
CHANGED
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
type TenantDb,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
7
7
|
import type {
|
|
8
|
+
ConfigCascade,
|
|
9
|
+
ConfigCascadeLevel,
|
|
8
10
|
ConfigKeyDefinition,
|
|
9
11
|
ConfigResolver,
|
|
12
|
+
ConfigStoredRowWithSource,
|
|
10
13
|
ConfigValueSource,
|
|
11
14
|
ConfigValueWithSource,
|
|
12
15
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
13
16
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
17
|
import { assertUnreachable, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
15
|
-
import { and, eq, isNull, or } from "drizzle-orm";
|
|
18
|
+
import { and, eq, inArray, isNull, or } from "drizzle-orm";
|
|
16
19
|
import { configValuesTable } from "./table";
|
|
17
20
|
|
|
18
21
|
type ConfigRow = {
|
|
@@ -66,6 +69,148 @@ export type ConfigResolverOptions = {
|
|
|
66
69
|
appOverrides?: AppConfigOverrides;
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
// Shared cascade-builder. Single-key path passes a `findRow`-bound row
|
|
73
|
+
// fetcher (one SQL per lookup); batch path passes a closure over
|
|
74
|
+
// pre-loaded rows. The builder itself is unaware of which.
|
|
75
|
+
async function buildCascade(
|
|
76
|
+
qualifiedKey: string,
|
|
77
|
+
keyDef: ConfigKeyDefinition,
|
|
78
|
+
tenantId: string,
|
|
79
|
+
userId: string,
|
|
80
|
+
db: DbConnection | TenantDb,
|
|
81
|
+
fetchRow: (
|
|
82
|
+
tenantId: string,
|
|
83
|
+
userId: string | null,
|
|
84
|
+
) => Promise<ConfigRow | null> | ConfigRow | null,
|
|
85
|
+
appOverrides: AppConfigOverrides | undefined,
|
|
86
|
+
encryption: EncryptionProvider | undefined,
|
|
87
|
+
): Promise<ConfigCascade> {
|
|
88
|
+
type Lookup = {
|
|
89
|
+
tenantId: string;
|
|
90
|
+
userId: string | null;
|
|
91
|
+
source: ConfigValueSource;
|
|
92
|
+
label: string;
|
|
93
|
+
};
|
|
94
|
+
const lookups: Lookup[] = [];
|
|
95
|
+
|
|
96
|
+
switch (keyDef.scope) {
|
|
97
|
+
case "user":
|
|
98
|
+
lookups.push({ tenantId, userId, source: "user-row", label: "User" });
|
|
99
|
+
lookups.push({ tenantId, userId: null, source: "tenant-row", label: "Tenant" });
|
|
100
|
+
break;
|
|
101
|
+
case "tenant":
|
|
102
|
+
lookups.push({ tenantId, userId: null, source: "tenant-row", label: "Tenant" });
|
|
103
|
+
lookups.push({
|
|
104
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
105
|
+
userId: null,
|
|
106
|
+
source: "system-row",
|
|
107
|
+
label: "System",
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
case "system":
|
|
111
|
+
lookups.push({
|
|
112
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
113
|
+
userId: null,
|
|
114
|
+
source: "system-row",
|
|
115
|
+
label: "System",
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
assertUnreachable(keyDef.scope, "config scope");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const levels: ConfigCascadeLevel[] = [];
|
|
123
|
+
let activeIndex = -1;
|
|
124
|
+
|
|
125
|
+
for (const lookup of lookups) {
|
|
126
|
+
const row = await fetchRow(lookup.tenantId, lookup.userId);
|
|
127
|
+
if (row?.value !== null && row?.value !== undefined) {
|
|
128
|
+
let raw = row.value;
|
|
129
|
+
if (keyDef.encrypted && encryption) {
|
|
130
|
+
raw = encryption.decrypt(raw);
|
|
131
|
+
}
|
|
132
|
+
if (activeIndex === -1) activeIndex = levels.length;
|
|
133
|
+
levels.push({
|
|
134
|
+
label: lookup.label,
|
|
135
|
+
value: deserializeValue(raw, keyDef.type),
|
|
136
|
+
source: lookup.source,
|
|
137
|
+
isActive: false,
|
|
138
|
+
hasValue: true,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
levels.push({
|
|
142
|
+
label: lookup.label,
|
|
143
|
+
value: undefined,
|
|
144
|
+
source: lookup.source,
|
|
145
|
+
isActive: false,
|
|
146
|
+
hasValue: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const overrideValue = appOverrides?.get(qualifiedKey);
|
|
152
|
+
const hasOverride = overrideValue !== undefined;
|
|
153
|
+
if (activeIndex === -1 && hasOverride) activeIndex = levels.length;
|
|
154
|
+
levels.push({
|
|
155
|
+
label: "App-Override",
|
|
156
|
+
value: overrideValue,
|
|
157
|
+
source: "app-override",
|
|
158
|
+
isActive: false,
|
|
159
|
+
hasValue: hasOverride,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (keyDef.computed) {
|
|
163
|
+
const value = await keyDef.computed({ tenantId, userId, db });
|
|
164
|
+
if (activeIndex === -1) activeIndex = levels.length;
|
|
165
|
+
levels.push({
|
|
166
|
+
label: "Computed",
|
|
167
|
+
value,
|
|
168
|
+
source: "computed",
|
|
169
|
+
isActive: false,
|
|
170
|
+
hasValue: true,
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
levels.push({
|
|
174
|
+
label: "Computed",
|
|
175
|
+
value: undefined,
|
|
176
|
+
source: "computed",
|
|
177
|
+
isActive: false,
|
|
178
|
+
hasValue: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (keyDef.default !== undefined) {
|
|
183
|
+
if (activeIndex === -1) activeIndex = levels.length;
|
|
184
|
+
levels.push({
|
|
185
|
+
label: "Default",
|
|
186
|
+
value: keyDef.default,
|
|
187
|
+
source: "default",
|
|
188
|
+
isActive: false,
|
|
189
|
+
hasValue: true,
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
if (activeIndex === -1) activeIndex = levels.length;
|
|
193
|
+
levels.push({
|
|
194
|
+
label: "Default",
|
|
195
|
+
value: undefined,
|
|
196
|
+
source: "missing",
|
|
197
|
+
isActive: false,
|
|
198
|
+
hasValue: false,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const active = activeIndex >= 0 ? levels[activeIndex] : undefined;
|
|
203
|
+
if (active !== undefined) {
|
|
204
|
+
levels[activeIndex] = { ...active, isActive: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
value: active?.value,
|
|
209
|
+
source: active?.source ?? "missing",
|
|
210
|
+
levels,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
69
214
|
export function createConfigResolver(options: ConfigResolverOptions = {}): ConfigResolver {
|
|
70
215
|
const { encryption, appOverrides } = options;
|
|
71
216
|
async function findRow(
|
|
@@ -193,6 +338,133 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
193
338
|
|
|
194
339
|
return result;
|
|
195
340
|
},
|
|
341
|
+
|
|
342
|
+
async getAllWithSource(tenantId, userId, db) {
|
|
343
|
+
// Load ALL potentially relevant rows (user + tenant + system)
|
|
344
|
+
const rows = await db
|
|
345
|
+
.select()
|
|
346
|
+
.from(configValuesTable)
|
|
347
|
+
.where(
|
|
348
|
+
or(
|
|
349
|
+
and(eq(configValuesTable.tenantId, SYSTEM_TENANT_ID), isNull(configValuesTable.userId)),
|
|
350
|
+
and(eq(configValuesTable.tenantId, tenantId), isNull(configValuesTable.userId)),
|
|
351
|
+
and(eq(configValuesTable.tenantId, tenantId), eq(configValuesTable.userId, userId)),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const result = new Map<string, ConfigStoredRowWithSource>();
|
|
356
|
+
|
|
357
|
+
// Group rows by key so we can determine the winner and its source
|
|
358
|
+
const groups = new Map<string, ConfigRow[]>();
|
|
359
|
+
for (const row of rows) {
|
|
360
|
+
const r = row as ConfigRow; // @cast-boundary db-row
|
|
361
|
+
const g = groups.get(r.key) ?? [];
|
|
362
|
+
g.push(r);
|
|
363
|
+
groups.set(r.key, g);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const [key, keyRows] of groups) {
|
|
367
|
+
const specificityOf = (candidate: ConfigRow) =>
|
|
368
|
+
(candidate.userId !== null ? 2 : 0) + (candidate.tenantId !== SYSTEM_TENANT_ID ? 1 : 0);
|
|
369
|
+
|
|
370
|
+
const first = keyRows[0];
|
|
371
|
+
if (!first) continue;
|
|
372
|
+
let winner: ConfigRow = first;
|
|
373
|
+
for (const r of keyRows) {
|
|
374
|
+
if (specificityOf(r) > specificityOf(winner)) {
|
|
375
|
+
winner = r;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let source: ConfigValueSource;
|
|
380
|
+
if (winner.userId !== null) {
|
|
381
|
+
source = "user-row";
|
|
382
|
+
} else if (winner.tenantId !== SYSTEM_TENANT_ID) {
|
|
383
|
+
source = "tenant-row";
|
|
384
|
+
} else {
|
|
385
|
+
source = "system-row";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
result.set(key, { ...winner, source });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return result;
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
async getCascade(qualifiedKey, keyDef, tenantId, userId, db): Promise<ConfigCascade> {
|
|
395
|
+
// Single-key path uses findRow per cascade step. The batch path
|
|
396
|
+
// bulk-loads all rows up-front; both build identical levels arrays.
|
|
397
|
+
return buildCascade(
|
|
398
|
+
qualifiedKey,
|
|
399
|
+
keyDef,
|
|
400
|
+
tenantId,
|
|
401
|
+
userId,
|
|
402
|
+
db,
|
|
403
|
+
(tid, uid) => findRow(qualifiedKey, tid, uid, db),
|
|
404
|
+
appOverrides,
|
|
405
|
+
encryption,
|
|
406
|
+
);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
async getCascadeBatch(
|
|
410
|
+
keys,
|
|
411
|
+
keyDefs,
|
|
412
|
+
tenantId,
|
|
413
|
+
userId,
|
|
414
|
+
db,
|
|
415
|
+
): Promise<ReadonlyMap<string, ConfigCascade>> {
|
|
416
|
+
if (keys.length === 0) return new Map();
|
|
417
|
+
|
|
418
|
+
// One SQL query for all keys + every scope (user-row,
|
|
419
|
+
// tenant-row, system-row). The cascade-builder then matches
|
|
420
|
+
// per-key from this preloaded set instead of querying again.
|
|
421
|
+
const rows = await db
|
|
422
|
+
.select()
|
|
423
|
+
.from(configValuesTable)
|
|
424
|
+
.where(
|
|
425
|
+
and(
|
|
426
|
+
inArray(configValuesTable.key, [...keys]),
|
|
427
|
+
or(
|
|
428
|
+
and(
|
|
429
|
+
eq(configValuesTable.tenantId, SYSTEM_TENANT_ID),
|
|
430
|
+
isNull(configValuesTable.userId),
|
|
431
|
+
),
|
|
432
|
+
and(eq(configValuesTable.tenantId, tenantId), isNull(configValuesTable.userId)),
|
|
433
|
+
and(eq(configValuesTable.tenantId, tenantId), eq(configValuesTable.userId, userId)),
|
|
434
|
+
),
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const grouped = new Map<string, ConfigRow[]>();
|
|
439
|
+
for (const row of rows) {
|
|
440
|
+
const r = row as ConfigRow; // @cast-boundary db-row
|
|
441
|
+
const g = grouped.get(r.key) ?? [];
|
|
442
|
+
g.push(r);
|
|
443
|
+
grouped.set(r.key, g);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = new Map<string, ConfigCascade>();
|
|
447
|
+
for (const key of keys) {
|
|
448
|
+
const keyDef = keyDefs.get(key);
|
|
449
|
+
if (!keyDef) continue;
|
|
450
|
+
|
|
451
|
+
const keyRows = grouped.get(key) ?? [];
|
|
452
|
+
const cascade = await buildCascade(
|
|
453
|
+
key,
|
|
454
|
+
keyDef,
|
|
455
|
+
tenantId,
|
|
456
|
+
userId,
|
|
457
|
+
db,
|
|
458
|
+
(tid, uid) =>
|
|
459
|
+
keyRows.find((r) => r.tenantId === tid && (r.userId ?? null) === uid) ?? null,
|
|
460
|
+
appOverrides,
|
|
461
|
+
encryption,
|
|
462
|
+
);
|
|
463
|
+
result.set(key, cascade);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return result;
|
|
467
|
+
},
|
|
196
468
|
};
|
|
197
469
|
}
|
|
198
470
|
|
|
@@ -129,20 +129,34 @@ async function fetchTemplate(
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function toPublic(row: TemplateResourceRow): TemplateResource {
|
|
132
|
+
// @cast-boundary db-row — Drizzle-Schema typisiert kind/contentFormat/
|
|
133
|
+
// scope/status als generic text. CHECK-Constraints in der DB schränken
|
|
134
|
+
// sie auf die Union-Types ein; Cast assertet das Schema-Wissen.
|
|
135
|
+
// linkedResources ist ein text-column mit JSON-payload (string→string map).
|
|
136
|
+
const kind = row.kind as RenderKind;
|
|
137
|
+
// @cast-boundary db-row — siehe kind.
|
|
138
|
+
const contentFormat = row.contentFormat as ContentFormat;
|
|
139
|
+
// @cast-boundary db-row — siehe kind.
|
|
140
|
+
const scope = row.scope as "system" | "tenant";
|
|
141
|
+
// @cast-boundary db-row — siehe kind.
|
|
142
|
+
const status = row.status as "draft" | "active" | "archived";
|
|
143
|
+
// @cast-boundary db-row — parseJson returnt Record<string, unknown>;
|
|
144
|
+
// linkedResources-Spalte enthält per Schema {key: signedUrl}-Map.
|
|
145
|
+
const linkedResources = parseJson(row.linkedResources) as Record<string, string>;
|
|
132
146
|
return {
|
|
133
147
|
id: String(row.id),
|
|
134
148
|
version: row.version,
|
|
135
149
|
tenantId: row.tenantId,
|
|
136
150
|
slug: row.slug,
|
|
137
|
-
kind
|
|
151
|
+
kind,
|
|
138
152
|
locale: row.locale,
|
|
139
153
|
content: row.content ?? "",
|
|
140
|
-
contentFormat
|
|
154
|
+
contentFormat,
|
|
141
155
|
variableSchema: parseJson(row.variableSchema),
|
|
142
|
-
linkedResources
|
|
143
|
-
scope
|
|
156
|
+
linkedResources,
|
|
157
|
+
scope,
|
|
144
158
|
parentTemplateId: row.parentTemplateId,
|
|
145
|
-
status
|
|
159
|
+
status,
|
|
146
160
|
updatedAt: row.updatedAt,
|
|
147
161
|
};
|
|
148
162
|
}
|
|
@@ -151,6 +165,8 @@ function parseJson(raw: string | null): Record<string, unknown> {
|
|
|
151
165
|
if (!raw) return {};
|
|
152
166
|
try {
|
|
153
167
|
const parsed = JSON.parse(raw);
|
|
168
|
+
// @cast-boundary engine-payload — JSON.parse returnt unknown, typeof-Guard
|
|
169
|
+
// grenzt auf object ein; Record<string, unknown> ist der minimale common-shape.
|
|
154
170
|
return typeof parsed === "object" && parsed !== null ? (parsed as Record<string, unknown>) : {};
|
|
155
171
|
} catch {
|
|
156
172
|
return {};
|
|
@@ -48,6 +48,8 @@ export const listQuery = defineQueryHandler({
|
|
|
48
48
|
|
|
49
49
|
const whereExpr = conditions.length > 0 ? and(...conditions) : undefined;
|
|
50
50
|
|
|
51
|
+
// @cast-boundary db-row — db.select returnt unknown[]; Row-Shape ist
|
|
52
|
+
// durch templateResourcesTable + buildBaseColumns garantiert.
|
|
51
53
|
const rows = (await ctx.db
|
|
52
54
|
.select()
|
|
53
55
|
.from(templateResourcesTable)
|
|
@@ -19,6 +19,8 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
19
19
|
access: { roles: ["SystemAdmin"] },
|
|
20
20
|
handler: async (event, ctx) => {
|
|
21
21
|
const db = ctx.db;
|
|
22
|
+
// @cast-boundary engine-payload — SYSTEM_TENANT_ID ist UUID-Literal,
|
|
23
|
+
// assert auf TenantId-Branded-Type (parseTenantId-Equivalent).
|
|
22
24
|
const tenantId = SYSTEM_TENANT_ID as TenantId;
|
|
23
25
|
// executor-user muss SYSTEM_TENANT als tenantId haben, sonst sucht
|
|
24
26
|
// event-store stream unter user.tenantId statt SYSTEM_TENANT → conflict.
|
|
@@ -63,10 +65,14 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
63
65
|
|
|
64
66
|
const result = await executor.create({ ...fields, tenantId }, executorUser, db);
|
|
65
67
|
if (!result.isSuccess) return result;
|
|
68
|
+
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
69
|
+
// INSERT RETURNING; shape { id } ist garantiert weil PK in der
|
|
70
|
+
// Returning-Klausel ist.
|
|
71
|
+
const createdRow = result.data as { id: string | number };
|
|
66
72
|
return {
|
|
67
73
|
isSuccess: true as const,
|
|
68
74
|
data: {
|
|
69
|
-
id: String(
|
|
75
|
+
id: String(createdRow.id),
|
|
70
76
|
slug: event.payload.slug,
|
|
71
77
|
isNew: true,
|
|
72
78
|
},
|
|
@@ -46,6 +46,9 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
46
46
|
}),
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
// @cast-boundary engine-payload — override aus Zod-parsed string,
|
|
50
|
+
// event.user.tenantId schon TenantId-branded; union als TenantId casten
|
|
51
|
+
// ist legit (override ist UUID-Format-validiert in schema).
|
|
49
52
|
const tenantId = (override ?? event.user.tenantId) as TenantId;
|
|
50
53
|
const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
|
|
51
54
|
|
|
@@ -86,10 +89,14 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
86
89
|
|
|
87
90
|
const result = await executor.create({ ...fields, tenantId }, executorUser, db);
|
|
88
91
|
if (!result.isSuccess) return result;
|
|
92
|
+
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
93
|
+
// INSERT RETURNING; shape { id } ist garantiert weil PK in der
|
|
94
|
+
// Returning-Klausel ist.
|
|
95
|
+
const createdRow = result.data as { id: string | number };
|
|
89
96
|
return {
|
|
90
97
|
isSuccess: true as const,
|
|
91
98
|
data: {
|
|
92
|
-
id: String(
|
|
99
|
+
id: String(createdRow.id),
|
|
93
100
|
slug: event.payload.slug,
|
|
94
101
|
isNew: true,
|
|
95
102
|
},
|