@cosmicdrift/kumiko-framework 0.154.0 → 0.154.2
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 +2 -2
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/api/__tests__/batch.integration.test.ts +9 -9
- package/src/engine/__tests__/engine.test.ts +16 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/define-feature.ts +22 -4
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +15 -28
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +55 -14
- package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
- package/src/engine/feature-ast/__tests__/patcher.test.ts +12 -22
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +247 -5
- package/src/engine/feature-ast/extractors/index.ts +0 -3
- package/src/engine/feature-ast/extractors/round4.ts +113 -287
- package/src/engine/feature-ast/index.ts +0 -4
- package/src/engine/feature-ast/parse.ts +0 -6
- package/src/engine/feature-ast/patch.ts +35 -45
- package/src/engine/feature-ast/patcher.ts +18 -40
- package/src/engine/feature-ast/patterns.ts +20 -41
- package/src/engine/feature-ast/render.ts +17 -27
- package/src/engine/feature-config-events-jobs.ts +150 -74
- package/src/engine/feature-entity-handlers.ts +24 -6
- package/src/engine/feature-ui-extensions.ts +116 -45
- package/src/engine/object-form.ts +12 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +0 -19
- package/src/engine/pattern-library/library.ts +0 -4
- package/src/engine/pattern-library/mixed-schemas.ts +9 -80
- package/src/engine/pattern-library/shared-fields.ts +1 -6
- package/src/engine/registry-ingest.ts +7 -2
- package/src/engine/registry-validate.ts +6 -6
- package/src/engine/types/feature.ts +76 -45
- package/src/engine/types/handlers.ts +6 -3
- package/src/engine/types/nav.ts +4 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/jobs/__tests__/jobs.integration.test.ts +66 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +1 -1
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +200 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/meilisearch-adapter.ts +13 -12
|
@@ -3,6 +3,7 @@ import { toTableName } from "../db/table-builder";
|
|
|
3
3
|
import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-handler";
|
|
4
4
|
import { type RegisterEntityCrudOptions, registerEntityCrud } from "./entity-handlers";
|
|
5
5
|
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
6
|
+
import { splitNamedDefinition } from "./object-form";
|
|
6
7
|
import type {
|
|
7
8
|
AccessRule,
|
|
8
9
|
EntityDefinition,
|
|
@@ -57,13 +58,17 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
57
58
|
) {
|
|
58
59
|
return {
|
|
59
60
|
entity(
|
|
60
|
-
|
|
61
|
-
definition
|
|
61
|
+
nameOrDefinition: string | ({ readonly name: string } & EntityDefinition),
|
|
62
|
+
definition?: EntityDefinition,
|
|
62
63
|
options?: { readonly table?: unknown },
|
|
63
64
|
): EntityRef {
|
|
64
|
-
|
|
65
|
+
const [entityName, entityDefinition] =
|
|
66
|
+
typeof nameOrDefinition === "string"
|
|
67
|
+
? [nameOrDefinition, definition as EntityDefinition]
|
|
68
|
+
: splitNamedDefinition(nameOrDefinition);
|
|
69
|
+
state.entities[entityName] = entityDefinition;
|
|
65
70
|
if (options?.table !== undefined) state.entityTables[entityName] = options.table;
|
|
66
|
-
return { name: entityName, table:
|
|
71
|
+
return { name: entityName, table: entityDefinition.table ?? toTableName(entityName) };
|
|
67
72
|
},
|
|
68
73
|
crud(
|
|
69
74
|
entityName: string,
|
|
@@ -73,10 +78,23 @@ export function buildEntityHandlerMethods<TName extends string>(
|
|
|
73
78
|
registerEntityCrud(getRegistrar(), entityName, definition, options);
|
|
74
79
|
return { name: entityName, table: definition.table ?? toTableName(entityName) };
|
|
75
80
|
},
|
|
76
|
-
relation(
|
|
81
|
+
relation(
|
|
82
|
+
entityRefOrDefinition:
|
|
83
|
+
| NameOrRef
|
|
84
|
+
| ({ readonly entity: NameOrRef; readonly name: string } & RelationDefinition),
|
|
85
|
+
relationName?: string,
|
|
86
|
+
definition?: RelationDefinition,
|
|
87
|
+
): void {
|
|
88
|
+
const [entityRef, resolvedRelationName, resolvedDefinition] =
|
|
89
|
+
typeof entityRefOrDefinition === "object" && "entity" in entityRefOrDefinition
|
|
90
|
+
? (() => {
|
|
91
|
+
const { entity, name: innerName, ...rest } = entityRefOrDefinition;
|
|
92
|
+
return [entity, innerName, rest as RelationDefinition] as const;
|
|
93
|
+
})()
|
|
94
|
+
: [entityRefOrDefinition, relationName as string, definition as RelationDefinition];
|
|
77
95
|
const entityName = resolveName(entityRef);
|
|
78
96
|
if (!state.relations[entityName]) state.relations[entityName] = {};
|
|
79
|
-
state.relations[entityName][
|
|
97
|
+
state.relations[entityName][resolvedRelationName] = resolvedDefinition;
|
|
80
98
|
},
|
|
81
99
|
writeHandler<TName extends string, TSchema extends ZodType>(
|
|
82
100
|
nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
|
|
@@ -31,6 +31,61 @@ import type { WorkspaceDefinition } from "./types/workspace";
|
|
|
31
31
|
|
|
32
32
|
// Builds hooks/extensions/projections/screens/nav/workspace/tables/tree-actions
|
|
33
33
|
// registrar methods.
|
|
34
|
+
|
|
35
|
+
type EntityWideHookType = "postSave" | "preDelete" | "postDelete" | "postQuery";
|
|
36
|
+
|
|
37
|
+
function isEntityWideHookType(type: LifecycleHookType | "validation"): type is EntityWideHookType {
|
|
38
|
+
return (
|
|
39
|
+
type === LifecycleHookTypes.postSave ||
|
|
40
|
+
type === LifecycleHookTypes.preDelete ||
|
|
41
|
+
type === LifecycleHookTypes.postDelete ||
|
|
42
|
+
type === LifecycleHookTypes.postQuery
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// r.hook(type, { allOf: entity }, fn) — "all write/query handlers of this
|
|
47
|
+
// entity", replacing the old r.entityHook(type, entity, fn). Hook-fn casts
|
|
48
|
+
// below: @cast-boundary engine-bridge — typed Dev-API (LifecycleHookFn) →
|
|
49
|
+
// erased Map<entityName, fn>.
|
|
50
|
+
function registerEntityWideHook(
|
|
51
|
+
state: FeatureBuilderState,
|
|
52
|
+
featureName: string,
|
|
53
|
+
type: EntityWideHookType,
|
|
54
|
+
entityName: string,
|
|
55
|
+
fn: LifecycleHookFn,
|
|
56
|
+
options?: { phase?: HookPhase },
|
|
57
|
+
): void {
|
|
58
|
+
if (type === LifecycleHookTypes.postSave) {
|
|
59
|
+
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
60
|
+
if (!state.entityPostSave[entityName]) state.entityPostSave[entityName] = [];
|
|
61
|
+
state.entityPostSave[entityName].push({
|
|
62
|
+
fn: fn as PostSaveHookFn,
|
|
63
|
+
phase,
|
|
64
|
+
featureName,
|
|
65
|
+
}); // @cast-boundary engine-bridge
|
|
66
|
+
} else if (type === LifecycleHookTypes.preDelete) {
|
|
67
|
+
if (!state.entityPreDelete[entityName]) state.entityPreDelete[entityName] = [];
|
|
68
|
+
state.entityPreDelete[entityName].push({
|
|
69
|
+
fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
|
|
70
|
+
phase: HookPhases.inTransaction,
|
|
71
|
+
featureName,
|
|
72
|
+
});
|
|
73
|
+
} else if (type === LifecycleHookTypes.postDelete) {
|
|
74
|
+
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
75
|
+
if (!state.entityPostDelete[entityName]) state.entityPostDelete[entityName] = [];
|
|
76
|
+
state.entityPostDelete[entityName].push({
|
|
77
|
+
fn: fn as PostDeleteHookFn,
|
|
78
|
+
phase,
|
|
79
|
+
featureName,
|
|
80
|
+
}); // @cast-boundary engine-bridge
|
|
81
|
+
} else {
|
|
82
|
+
// postQuery is unphased (no inTransaction/afterCommit semantics — fires
|
|
83
|
+
// synchronously after query-handler-execute, before field-access-filter)
|
|
84
|
+
if (!state.entityPostQuery[entityName]) state.entityPostQuery[entityName] = [];
|
|
85
|
+
state.entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName }); // @cast-boundary engine-bridge
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
34
89
|
export function buildUiExtensionsMethods<TName extends string>(
|
|
35
90
|
state: FeatureBuilderState,
|
|
36
91
|
name: TName,
|
|
@@ -38,10 +93,36 @@ export function buildUiExtensionsMethods<TName extends string>(
|
|
|
38
93
|
return {
|
|
39
94
|
hook(
|
|
40
95
|
type: LifecycleHookType | "validation",
|
|
41
|
-
target: NameOrRef | readonly NameOrRef[],
|
|
96
|
+
target: NameOrRef | readonly NameOrRef[] | { readonly allOf: NameOrRef },
|
|
42
97
|
fn: LifecycleHookFn | ValidationHookFn,
|
|
43
98
|
options?: { phase?: HookPhase },
|
|
44
99
|
): void {
|
|
100
|
+
// Entity-wide target ("all write/query handlers of this entity") —
|
|
101
|
+
// replaces the old r.entityHook(type, entity, fn).
|
|
102
|
+
if (
|
|
103
|
+
typeof target === "object" &&
|
|
104
|
+
target !== null &&
|
|
105
|
+
!Array.isArray(target) &&
|
|
106
|
+
"allOf" in target
|
|
107
|
+
) {
|
|
108
|
+
if (!isEntityWideHookType(type)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[Feature ${name}] r.hook("${type}", { allOf }, ...) only supports ` +
|
|
111
|
+
`postSave/preDelete/postDelete/postQuery, not "${type}".`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
registerEntityWideHook(
|
|
115
|
+
state,
|
|
116
|
+
name,
|
|
117
|
+
type,
|
|
118
|
+
resolveName(target.allOf),
|
|
119
|
+
fn as LifecycleHookFn,
|
|
120
|
+
options,
|
|
121
|
+
);
|
|
122
|
+
// skip: entity-wide target fully handled above, nothing more to do
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
45
126
|
const targets = Array.isArray(target) ? target : [target];
|
|
46
127
|
const names = targets.map(resolveName);
|
|
47
128
|
|
|
@@ -81,43 +162,6 @@ export function buildUiExtensionsMethods<TName extends string>(
|
|
|
81
162
|
bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
|
|
82
163
|
}
|
|
83
164
|
},
|
|
84
|
-
entityHook(
|
|
85
|
-
type: "postSave" | "preDelete" | "postDelete" | "postQuery",
|
|
86
|
-
entityRef: NameOrRef,
|
|
87
|
-
fn: LifecycleHookFn,
|
|
88
|
-
options?: { phase?: HookPhase },
|
|
89
|
-
): void {
|
|
90
|
-
const entityName = resolveName(entityRef);
|
|
91
|
-
if (type === LifecycleHookTypes.postSave) {
|
|
92
|
-
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
93
|
-
if (!state.entityPostSave[entityName]) state.entityPostSave[entityName] = [];
|
|
94
|
-
state.entityPostSave[entityName].push({
|
|
95
|
-
fn: fn as PostSaveHookFn,
|
|
96
|
-
phase,
|
|
97
|
-
featureName: name,
|
|
98
|
-
}); // @cast-boundary engine-bridge
|
|
99
|
-
} else if (type === LifecycleHookTypes.preDelete) {
|
|
100
|
-
if (!state.entityPreDelete[entityName]) state.entityPreDelete[entityName] = [];
|
|
101
|
-
state.entityPreDelete[entityName].push({
|
|
102
|
-
fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
|
|
103
|
-
phase: HookPhases.inTransaction,
|
|
104
|
-
featureName: name,
|
|
105
|
-
});
|
|
106
|
-
} else if (type === LifecycleHookTypes.postDelete) {
|
|
107
|
-
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
108
|
-
if (!state.entityPostDelete[entityName]) state.entityPostDelete[entityName] = [];
|
|
109
|
-
state.entityPostDelete[entityName].push({
|
|
110
|
-
fn: fn as PostDeleteHookFn,
|
|
111
|
-
phase,
|
|
112
|
-
featureName: name,
|
|
113
|
-
}); // @cast-boundary engine-bridge
|
|
114
|
-
} else if (type === LifecycleHookTypes.postQuery) {
|
|
115
|
-
// postQuery is unphased (no inTransaction/afterCommit semantics — fires
|
|
116
|
-
// synchronously after query-handler-execute, before field-access-filter)
|
|
117
|
-
if (!state.entityPostQuery[entityName]) state.entityPostQuery[entityName] = [];
|
|
118
|
-
state.entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName: name }); // @cast-boundary engine-bridge
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
165
|
searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void {
|
|
122
166
|
const entityName = resolveName(entityRef);
|
|
123
167
|
if (!state.searchPayloadExtensions[entityName])
|
|
@@ -128,11 +172,24 @@ export function buildUiExtensionsMethods<TName extends string>(
|
|
|
128
172
|
state.registrarExtensions[extensionName] = def;
|
|
129
173
|
},
|
|
130
174
|
useExtension(
|
|
131
|
-
|
|
132
|
-
|
|
175
|
+
extensionNameOrDefinition:
|
|
176
|
+
| string
|
|
177
|
+
| ({ readonly name: string; readonly entity: NameOrRef } & Record<string, unknown>),
|
|
178
|
+
entityRef?: NameOrRef,
|
|
133
179
|
options?: Record<string, unknown>,
|
|
134
180
|
): void {
|
|
135
|
-
|
|
181
|
+
const [extensionName, resolvedEntityRef, resolvedOptions] =
|
|
182
|
+
typeof extensionNameOrDefinition === "string"
|
|
183
|
+
? [extensionNameOrDefinition, entityRef as NameOrRef, options]
|
|
184
|
+
: (() => {
|
|
185
|
+
const { name, entity, ...rest } = extensionNameOrDefinition;
|
|
186
|
+
return [name, entity, rest] as const;
|
|
187
|
+
})();
|
|
188
|
+
state.extensionUsages.push({
|
|
189
|
+
extensionName,
|
|
190
|
+
entityName: resolveName(resolvedEntityRef),
|
|
191
|
+
options: resolvedOptions,
|
|
192
|
+
});
|
|
136
193
|
},
|
|
137
194
|
extensionSelector(extensionName: string, key: { readonly name: string } | string): void {
|
|
138
195
|
if (state.extensionSelectors.some((s) => s.extensionName === extensionName)) {
|
|
@@ -232,14 +289,28 @@ export function buildUiExtensionsMethods<TName extends string>(
|
|
|
232
289
|
state.entityProjectionExtensions[entityName] = list;
|
|
233
290
|
},
|
|
234
291
|
referenceData(
|
|
235
|
-
|
|
236
|
-
|
|
292
|
+
entityRefOrDefinition:
|
|
293
|
+
| NameOrRef
|
|
294
|
+
| {
|
|
295
|
+
readonly entity: NameOrRef;
|
|
296
|
+
readonly data: readonly Record<string, unknown>[];
|
|
297
|
+
readonly upsertKey?: string;
|
|
298
|
+
},
|
|
299
|
+
data?: readonly Record<string, unknown>[],
|
|
237
300
|
options?: { upsertKey?: string },
|
|
238
301
|
): void {
|
|
302
|
+
const [entityRef, resolvedData, upsertKey] =
|
|
303
|
+
typeof entityRefOrDefinition === "object" && "entity" in entityRefOrDefinition
|
|
304
|
+
? [
|
|
305
|
+
entityRefOrDefinition.entity,
|
|
306
|
+
entityRefOrDefinition.data,
|
|
307
|
+
entityRefOrDefinition.upsertKey,
|
|
308
|
+
]
|
|
309
|
+
: [entityRefOrDefinition, data as readonly Record<string, unknown>[], options?.upsertKey];
|
|
239
310
|
state.referenceData.push({
|
|
240
311
|
entityName: resolveName(entityRef),
|
|
241
|
-
data,
|
|
242
|
-
upsertKey
|
|
312
|
+
data: resolvedData,
|
|
313
|
+
upsertKey,
|
|
243
314
|
});
|
|
244
315
|
},
|
|
245
316
|
screen(definition: ScreenDefinition): void {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Shared helper for registrar methods that accept an Object-Form call
|
|
2
|
+
// (`r.entity({ name: "item", ... })`) alongside the classic positional form
|
|
3
|
+
// (`r.entity("item", { ... })`). Object-Form is the shape the feature-ast
|
|
4
|
+
// renderer emits for Designer/AI-generated code — a single object argument
|
|
5
|
+
// with named fields is easier to generate correctly than positional args
|
|
6
|
+
// whose count/order vary per method.
|
|
7
|
+
export function splitNamedDefinition<T extends { readonly name: string }>(
|
|
8
|
+
definition: T,
|
|
9
|
+
): [string, Omit<T, "name">] {
|
|
10
|
+
const { name, ...rest } = definition;
|
|
11
|
+
return [name, rest];
|
|
12
|
+
}
|
|
@@ -47,7 +47,6 @@ const ALL_KINDS: FeaturePatternKind[] = [
|
|
|
47
47
|
"writeHandler",
|
|
48
48
|
"queryHandler",
|
|
49
49
|
"hook",
|
|
50
|
-
"entityHook",
|
|
51
50
|
"job",
|
|
52
51
|
"notification",
|
|
53
52
|
"authClaims",
|
|
@@ -55,7 +54,6 @@ const ALL_KINDS: FeaturePatternKind[] = [
|
|
|
55
54
|
"projection",
|
|
56
55
|
"multiStreamProjection",
|
|
57
56
|
"defineEvent",
|
|
58
|
-
"eventMigration",
|
|
59
57
|
"extendsRegistrar",
|
|
60
58
|
"usesApi",
|
|
61
59
|
"exposesApi",
|
|
@@ -280,14 +278,6 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
280
278
|
target: "x",
|
|
281
279
|
fnBody: PLACEHOLDER_BODY_LOC,
|
|
282
280
|
};
|
|
283
|
-
case "entityHook":
|
|
284
|
-
return {
|
|
285
|
-
kind,
|
|
286
|
-
source: PLACEHOLDER_LOC,
|
|
287
|
-
hookType: "postSave",
|
|
288
|
-
entityName: "x",
|
|
289
|
-
fnBody: PLACEHOLDER_BODY_LOC,
|
|
290
|
-
};
|
|
291
281
|
case "job":
|
|
292
282
|
return {
|
|
293
283
|
kind,
|
|
@@ -332,15 +322,6 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
332
322
|
eventName: "x",
|
|
333
323
|
schemaSource: PLACEHOLDER_BODY_LOC,
|
|
334
324
|
};
|
|
335
|
-
case "eventMigration":
|
|
336
|
-
return {
|
|
337
|
-
kind,
|
|
338
|
-
source: PLACEHOLDER_LOC,
|
|
339
|
-
eventName: "x",
|
|
340
|
-
fromVersion: 1,
|
|
341
|
-
toVersion: 2,
|
|
342
|
-
transformBody: PLACEHOLDER_BODY_LOC,
|
|
343
|
-
};
|
|
344
325
|
case "extendsRegistrar":
|
|
345
326
|
return {
|
|
346
327
|
kind,
|
|
@@ -17,8 +17,6 @@ import type { FeaturePatternKind } from "../feature-ast/patterns";
|
|
|
17
17
|
import {
|
|
18
18
|
authClaimsSchema,
|
|
19
19
|
defineEventSchema,
|
|
20
|
-
entityHookSchema,
|
|
21
|
-
eventMigrationSchema,
|
|
22
20
|
hookSchema,
|
|
23
21
|
httpRouteSchema,
|
|
24
22
|
jobSchema,
|
|
@@ -82,7 +80,6 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
82
80
|
writeHandler: writeHandlerSchema,
|
|
83
81
|
queryHandler: queryHandlerSchema,
|
|
84
82
|
hook: hookSchema,
|
|
85
|
-
entityHook: entityHookSchema,
|
|
86
83
|
job: jobSchema,
|
|
87
84
|
notification: notificationSchema,
|
|
88
85
|
authClaims: authClaimsSchema,
|
|
@@ -90,7 +87,6 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
90
87
|
projection: projectionSchema,
|
|
91
88
|
multiStreamProjection: multiStreamProjectionSchema,
|
|
92
89
|
defineEvent: defineEventSchema,
|
|
93
|
-
eventMigration: eventMigrationSchema,
|
|
94
90
|
extendsRegistrar: extendsRegistrarSchema,
|
|
95
91
|
usesApi: usesApiSchema,
|
|
96
92
|
exposesApi: exposesApiSchema,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// Mixed pattern schemas (header form + opaque body source).
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
accessRuleField,
|
|
5
|
-
ENTITY_HOOK_TYPE_OPTIONS,
|
|
6
|
-
HOOK_TYPE_OPTIONS,
|
|
7
|
-
HTTP_METHOD_OPTIONS,
|
|
8
|
-
} from "./shared-fields";
|
|
3
|
+
import { accessRuleField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS } from "./shared-fields";
|
|
9
4
|
import type { PatternFormSchema } from "./types";
|
|
10
5
|
|
|
11
6
|
// --- Mixed patterns (header form + opaque body source) --------------------
|
|
@@ -169,45 +164,6 @@ export const hookSchema: PatternFormSchema = {
|
|
|
169
164
|
],
|
|
170
165
|
};
|
|
171
166
|
|
|
172
|
-
export const entityHookSchema: PatternFormSchema = {
|
|
173
|
-
kind: "entityHook",
|
|
174
|
-
label: { en: "Entity hook", de: "Entity-Hook" },
|
|
175
|
-
summary: { en: "Hook scoped to a single entity (no cross-entity targets)." },
|
|
176
|
-
category: "behaviour",
|
|
177
|
-
editability: "mixed",
|
|
178
|
-
fields: [
|
|
179
|
-
{
|
|
180
|
-
path: "hookType",
|
|
181
|
-
label: { en: "Type", de: "Typ" },
|
|
182
|
-
input: "select",
|
|
183
|
-
options: ENTITY_HOOK_TYPE_OPTIONS,
|
|
184
|
-
required: true,
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
path: "entityName",
|
|
188
|
-
label: { en: "Entity", de: "Entität" },
|
|
189
|
-
input: "entity-ref",
|
|
190
|
-
required: true,
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
path: "fnBody",
|
|
194
|
-
label: { en: "Hook body (source)", de: "Hook-Body (Source)" },
|
|
195
|
-
input: "code-block",
|
|
196
|
-
language: "typescript",
|
|
197
|
-
readOnly: true,
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
path: "phase",
|
|
201
|
-
label: { en: "Phase", de: "Phase" },
|
|
202
|
-
input: "select",
|
|
203
|
-
options: [
|
|
204
|
-
{ value: "inTransaction", label: { en: "In transaction" } },
|
|
205
|
-
{ value: "afterCommit", label: { en: "After commit" } },
|
|
206
|
-
],
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
|
|
211
167
|
export const jobSchema: PatternFormSchema = {
|
|
212
168
|
kind: "job",
|
|
213
169
|
label: { en: "Job", de: "Job" },
|
|
@@ -443,42 +399,15 @@ export const defineEventSchema: PatternFormSchema = {
|
|
|
443
399
|
input: "number",
|
|
444
400
|
min: 1,
|
|
445
401
|
},
|
|
446
|
-
],
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
export const eventMigrationSchema: PatternFormSchema = {
|
|
450
|
-
kind: "eventMigration",
|
|
451
|
-
label: { en: "Event migration", de: "Event-Migration" },
|
|
452
|
-
summary: { en: "Step-wise transform between event versions." },
|
|
453
|
-
category: "data",
|
|
454
|
-
editability: "mixed",
|
|
455
|
-
fields: [
|
|
456
|
-
{
|
|
457
|
-
path: "eventName",
|
|
458
|
-
label: { en: "Event", de: "Event" },
|
|
459
|
-
input: "text",
|
|
460
|
-
required: true,
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
path: "fromVersion",
|
|
464
|
-
label: { en: "From version", de: "Von Version" },
|
|
465
|
-
input: "number",
|
|
466
|
-
min: 1,
|
|
467
|
-
required: true,
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
path: "toVersion",
|
|
471
|
-
label: { en: "To version", de: "Auf Version" },
|
|
472
|
-
input: "number",
|
|
473
|
-
min: 2,
|
|
474
|
-
required: true,
|
|
475
|
-
},
|
|
476
402
|
{
|
|
477
|
-
path: "
|
|
478
|
-
label: {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
403
|
+
path: "migrations",
|
|
404
|
+
label: {
|
|
405
|
+
en: "Migrations (fromVersion → transform)",
|
|
406
|
+
de: "Migrationen (fromVersion → Transform)",
|
|
407
|
+
},
|
|
408
|
+
input: "key-value-map",
|
|
409
|
+
keyPlaceholder: "1",
|
|
410
|
+
valueInput: "code-block",
|
|
482
411
|
},
|
|
483
412
|
],
|
|
484
413
|
};
|
|
@@ -11,12 +11,7 @@ export const HOOK_TYPE_OPTIONS = [
|
|
|
11
11
|
{ value: "preDelete", label: { en: "Pre-Delete", de: "Vor Löschen" } },
|
|
12
12
|
{ value: "postDelete", label: { en: "Post-Delete", de: "Nach Löschen" } },
|
|
13
13
|
{ value: "preQuery", label: { en: "Pre-Query", de: "Vor Abfrage" } },
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export const ENTITY_HOOK_TYPE_OPTIONS = [
|
|
17
|
-
{ value: "postSave", label: { en: "Post-Save", de: "Nach Speichern" } },
|
|
18
|
-
{ value: "preDelete", label: { en: "Pre-Delete", de: "Vor Löschen" } },
|
|
19
|
-
{ value: "postDelete", label: { en: "Post-Delete", de: "Nach Löschen" } },
|
|
14
|
+
{ value: "postQuery", label: { en: "Post-Query", de: "Nach Abfrage" } },
|
|
20
15
|
] as const;
|
|
21
16
|
|
|
22
17
|
export const HTTP_METHOD_OPTIONS = [
|
|
@@ -147,9 +147,14 @@ export function populateEvents(state: RegistryState, feature: FeatureDefinition)
|
|
|
147
147
|
|
|
148
148
|
// Translations prefixed with featureName: (i18next namespace convention).
|
|
149
149
|
export function populateTranslations(state: RegistryState, feature: FeatureDefinition): void {
|
|
150
|
-
// Translations prefixed with featureName: (i18next namespace convention)
|
|
150
|
+
// Translations prefixed with featureName: (i18next namespace convention).
|
|
151
|
+
// Keys that already carry the feature's own namespace prefix (e.g. a nav
|
|
152
|
+
// label referencing "cap-counter:nav.cap-list" verbatim) must NOT be
|
|
153
|
+
// re-prefixed, else server-side t() can never resolve them (#1105).
|
|
154
|
+
const prefix = `${feature.name}:`;
|
|
151
155
|
for (const [key, value] of Object.entries(feature.translations ?? {})) {
|
|
152
|
-
|
|
156
|
+
const qualifiedKey = key.startsWith(prefix) ? key : `${prefix}${key}`;
|
|
157
|
+
state.mergedTranslations[qualifiedKey] = value;
|
|
153
158
|
}
|
|
154
159
|
}
|
|
155
160
|
|
|
@@ -314,23 +314,23 @@ export function validateEventMigrationVersions(
|
|
|
314
314
|
features: readonly FeatureDefinition[],
|
|
315
315
|
): void {
|
|
316
316
|
// Build + validate event upcaster chains. Run AFTER all features are
|
|
317
|
-
// ingested so
|
|
318
|
-
// feature (same feature in practice, but the check stays lax for
|
|
319
|
-
// cross-feature event packs).
|
|
317
|
+
// ingested so defineEvent's `migrations` option can reference events from
|
|
318
|
+
// any feature (same feature in practice, but the check stays lax for
|
|
319
|
+
// future cross-feature event packs).
|
|
320
320
|
for (const feature of features) {
|
|
321
321
|
for (const [shortName, migrations] of Object.entries(feature.eventMigrations ?? {})) {
|
|
322
322
|
const qualified = qualify(feature.name, "event", shortName);
|
|
323
323
|
const eventDef = state.eventMap.get(qualified);
|
|
324
324
|
if (!eventDef) {
|
|
325
325
|
throw new Error(
|
|
326
|
-
`Feature "${feature.name}"
|
|
326
|
+
`Feature "${feature.name}" has migrations declared for event "${shortName}" ` +
|
|
327
327
|
`but no r.defineEvent exists for that name. Register the event first.`,
|
|
328
328
|
);
|
|
329
329
|
}
|
|
330
330
|
for (const m of migrations) {
|
|
331
331
|
if (m.toVersion > eventDef.version) {
|
|
332
332
|
throw new Error(
|
|
333
|
-
`Feature "${feature.name}"
|
|
333
|
+
`Feature "${feature.name}" declares a migration for event "${shortName}" from v${m.fromVersion} to v${m.toVersion} ` +
|
|
334
334
|
`but r.defineEvent declares only version ${eventDef.version}. ` +
|
|
335
335
|
`Bump the version in defineEvent to at least ${m.toVersion}, or remove the migration.`,
|
|
336
336
|
);
|
|
@@ -364,7 +364,7 @@ export function buildEventUpcasterChains(
|
|
|
364
364
|
if (!chainMap.has(v)) {
|
|
365
365
|
throw new Error(
|
|
366
366
|
`Event "${qualified}" declares version ${eventDef.version} but no migration ` +
|
|
367
|
-
`covers the step v${v} → v${v + 1}.
|
|
367
|
+
`covers the step v${v} → v${v + 1}. Add { fromVersion: ${v}, toVersion: ${v + 1}, transform } to r.defineEvent("${qualified.split(":").pop() ?? qualified}", ...)'s migrations array ` +
|
|
368
368
|
`so stored v${v} payloads can be upcast on read.`,
|
|
369
369
|
);
|
|
370
370
|
}
|