@cosmicdrift/kumiko-framework 0.238.0 → 0.240.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/package.json +3 -3
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +381 -0
- package/src/db/event-store-executor-read.ts +186 -34
- package/src/engine/__tests__/engine.test.ts +77 -28
- package/src/engine/__tests__/screen.test.ts +455 -0
- package/src/engine/boot-validator/screens.ts +111 -1
- package/src/engine/entity-handlers.ts +7 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/registry-facade.ts +3 -3
- package/src/engine/registry-state.ts +13 -2
- package/src/engine/registry-validate.ts +47 -14
- package/src/engine/registry.ts +2 -2
- package/src/engine/screen-helpers.ts +5 -0
- package/src/engine/types/index.ts +1 -0
- package/src/i18n/required-surface-keys.ts +20 -1
- package/src/ui-types/index.ts +6 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { configureEventPiiCatalog } from "../crypto/event-pii";
|
|
2
2
|
import { resolveName } from "./handler-helpers";
|
|
3
|
-
import type { RegistryState } from "./registry-state";
|
|
3
|
+
import type { RegistryState, SearchableReferenceField } from "./registry-state";
|
|
4
4
|
import { buildImplicitProjection, hasFieldAccessRules, qualify } from "./registry-state";
|
|
5
5
|
import {
|
|
6
6
|
buildSoftDeleteCleanupJob,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SOFT_DELETE_GRACE_DAYS_KEY,
|
|
11
11
|
softDeleteGraceDaysConfig,
|
|
12
12
|
} from "./soft-delete-cleanup";
|
|
13
|
-
import type { EventPiiFields, EventUpcastFn, FeatureDefinition } from "./types";
|
|
13
|
+
import type { EventPiiFields, EventUpcastFn, FeatureDefinition, ReferenceFieldDef } from "./types";
|
|
14
14
|
import { HookPhases } from "./types";
|
|
15
15
|
|
|
16
16
|
function allHandlerQns(state: RegistryState): ReadonlySet<string> {
|
|
@@ -154,13 +154,50 @@ export function applyExtensionUsages(state: RegistryState): void {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
// "customer" (same-feature) or "users:customer" (cross-feature,
|
|
158
|
+
// "<featureName>:<entityName>") — only the entity name matters here, names
|
|
159
|
+
// are globally unique in entityMap. Local duplicate of the same parse done
|
|
160
|
+
// in db/eagerload.ts's parseRefEntity — this loop has no feature context to
|
|
161
|
+
// hand to engine/parse-ref-target.ts's parseRefTarget.
|
|
162
|
+
function parseReferenceTargetEntityName(raw: string): string {
|
|
163
|
+
const idx = raw.indexOf(":");
|
|
164
|
+
return idx < 0 ? raw : raw.slice(idx + 1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// fw#2660: labelField defaults to "id" (a UUID column) elsewhere, but a
|
|
168
|
+
// searchable reference ILIKEs the label column — an implicit or explicit
|
|
169
|
+
// "id" default would 500 on every search request. Fail at boot.
|
|
170
|
+
function buildSearchableReferenceField(
|
|
171
|
+
entityName: string,
|
|
172
|
+
fieldName: string,
|
|
173
|
+
field: ReferenceFieldDef,
|
|
174
|
+
): SearchableReferenceField {
|
|
175
|
+
if (field.labelField === undefined || field.labelField === "id") {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`[Entity ${entityName}] field "${fieldName}": searchable reference fields require an ` +
|
|
178
|
+
`explicit, non-"id" labelField. "id" is a UUID column — ILIKE against it fails ` +
|
|
179
|
+
`at request time. Set labelField to a human-readable field on the referenced entity.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (field.multiple === true) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`[Entity ${entityName}] field "${fieldName}": searchable is not supported on multiple ` +
|
|
185
|
+
`(array) reference fields — remove "multiple" or "searchable".`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
fieldName,
|
|
190
|
+
targetEntityName: parseReferenceTargetEntityName(field.entity),
|
|
191
|
+
labelField: field.labelField,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
160
194
|
|
|
195
|
+
// Precompute: searchable/sortable fields, searchable reference fields.
|
|
196
|
+
export function buildSearchableSortableCaches(state: RegistryState): void {
|
|
161
197
|
for (const [name, entity] of state.entityMap) {
|
|
162
198
|
const searchable: string[] = [];
|
|
163
199
|
const sortable: string[] = [];
|
|
200
|
+
const searchableReferences: SearchableReferenceField[] = [];
|
|
164
201
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
165
202
|
if (field.type === "text" && field.searchable === true) searchable.push(fieldName);
|
|
166
203
|
if (field.type === "text" && field.sortable === true) sortable.push(fieldName);
|
|
@@ -169,9 +206,13 @@ export function buildSearchableSortableCaches(state: RegistryState): void {
|
|
|
169
206
|
if (subField.searchable === true) searchable.push(`${fieldName}_${subName}`);
|
|
170
207
|
}
|
|
171
208
|
}
|
|
209
|
+
if (field.type === "reference" && field.searchable === true) {
|
|
210
|
+
searchableReferences.push(buildSearchableReferenceField(name, fieldName, field));
|
|
211
|
+
}
|
|
172
212
|
}
|
|
173
213
|
state.searchableFieldsCache.set(name, searchable);
|
|
174
214
|
state.sortableFieldsCache.set(name, sortable);
|
|
215
|
+
state.searchableReferencesCache.set(name, searchableReferences);
|
|
175
216
|
}
|
|
176
217
|
}
|
|
177
218
|
|
|
@@ -223,16 +264,8 @@ export function buildImplicitProjections(
|
|
|
223
264
|
}
|
|
224
265
|
}
|
|
225
266
|
|
|
226
|
-
export function
|
|
267
|
+
export function buildIncomingRelations(state: RegistryState): void {
|
|
227
268
|
for (const [entityName, rels] of state.relationMap) {
|
|
228
|
-
const includes = new Map<string, readonly string[]>();
|
|
229
|
-
for (const [relName, rel] of Object.entries(rels)) {
|
|
230
|
-
if ((rel.type === "belongsTo" || rel.type === "manyToMany") && rel.searchInclude?.length) {
|
|
231
|
-
includes.set(relName, rel.searchInclude);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
state.searchIncludesCache.set(entityName, includes);
|
|
235
|
-
|
|
236
269
|
// Build reverse index for incoming relations
|
|
237
270
|
for (const [relName, rel] of Object.entries(rels)) {
|
|
238
271
|
const existing = state.incomingRelationsCache.get(rel.target) ?? [];
|
package/src/engine/registry.ts
CHANGED
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
autoWireSoftDeleteJobs,
|
|
21
21
|
buildEventUpcasterChains,
|
|
22
22
|
buildImplicitProjections,
|
|
23
|
+
buildIncomingRelations,
|
|
23
24
|
buildSearchableSortableCaches,
|
|
24
|
-
buildSearchIncludesAndIncomingRelations,
|
|
25
25
|
computeHasRateLimitedHandler,
|
|
26
26
|
finalizeWorkspaceNavMembership,
|
|
27
27
|
populateHandlerEntityMappings,
|
|
@@ -68,7 +68,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
68
68
|
applyExtensionUsages(state);
|
|
69
69
|
buildSearchableSortableCaches(state);
|
|
70
70
|
buildImplicitProjections(state, features);
|
|
71
|
-
|
|
71
|
+
buildIncomingRelations(state);
|
|
72
72
|
validateFieldAccessHandlersAreEntityMapped(state, features);
|
|
73
73
|
validateExtensionPreSaveWiring(features);
|
|
74
74
|
validateRelationTargetsExist(state);
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
EditFieldSpec,
|
|
4
4
|
EditFieldsSection,
|
|
5
5
|
EditSectionSpec,
|
|
6
|
+
EditWriteFormSection,
|
|
6
7
|
FieldCondition,
|
|
7
8
|
FormatSpec,
|
|
8
9
|
ListColumnSpec,
|
|
@@ -16,6 +17,10 @@ export function isFieldsEditSection(section: EditSectionSpec): section is EditFi
|
|
|
16
17
|
return section.kind === undefined || section.kind === "fields";
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export function isWriteFormEditSection(section: EditSectionSpec): section is EditWriteFormSection {
|
|
21
|
+
return section.kind === "writeForm";
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
// Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
|
|
20
25
|
// authors who branch on the three FieldRenderer variants without manual
|
|
21
26
|
// "format" in renderer checks.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isExtensionEditSection,
|
|
3
|
+
isWriteFormEditSection,
|
|
4
|
+
normalizeListColumn,
|
|
5
|
+
} from "../engine/screen-helpers";
|
|
2
6
|
import type {
|
|
3
7
|
ActionFormScreenDefinition,
|
|
4
8
|
ConfigEditScreenDefinition,
|
|
@@ -27,6 +31,11 @@ export const CONFIG_EDIT_ENTITY = "__config-edit__";
|
|
|
27
31
|
/** Pseudo-entity for projectionDetail field labels (renderer projection-detail-shim). */
|
|
28
32
|
export const PROJECTION_DETAIL_ENTITY = "__projection-detail__";
|
|
29
33
|
|
|
34
|
+
/** Pseudo-entity for a projectionDetail writeForm section's own fieldDefs
|
|
35
|
+
* (distinct from PROJECTION_DETAIL_ENTITY — the section's fields aren't
|
|
36
|
+
* drawn from the host record's display entity). */
|
|
37
|
+
export const WRITE_FORM_SECTION_ENTITY = "__write-form-section__";
|
|
38
|
+
|
|
30
39
|
export function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string {
|
|
31
40
|
return `${featureName}:entity:${entityName}:field:${fieldName}`;
|
|
32
41
|
}
|
|
@@ -247,6 +256,16 @@ export function requiredKeysFromScreen(
|
|
|
247
256
|
const normalized = normalizeListColumn(col);
|
|
248
257
|
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
249
258
|
}
|
|
259
|
+
for (const action of section.rowActions ?? []) pushRowActionKeys(out, action);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (isWriteFormEditSection(section)) {
|
|
263
|
+
pushKey(out, section.title);
|
|
264
|
+
pushKey(out, section.submitLabel);
|
|
265
|
+
for (const f of section.fields) {
|
|
266
|
+
const fieldName = editFieldName(f);
|
|
267
|
+
out.add(fieldLabelKey(featureName, WRITE_FORM_SECTION_ENTITY, fieldName));
|
|
268
|
+
}
|
|
250
269
|
continue;
|
|
251
270
|
}
|
|
252
271
|
pushKey(out, section.title);
|
package/src/ui-types/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
isExtensionEditSection,
|
|
31
31
|
isFieldsEditSection,
|
|
32
32
|
isFormatSpec,
|
|
33
|
+
isWriteFormEditSection,
|
|
33
34
|
normalizeEditField,
|
|
34
35
|
normalizeListColumn,
|
|
35
36
|
} from "../engine/screen-helpers";
|
|
@@ -74,6 +75,7 @@ export type {
|
|
|
74
75
|
EditLayout,
|
|
75
76
|
EditRelatedListSection,
|
|
76
77
|
EditSectionSpec,
|
|
78
|
+
EditWriteFormSection,
|
|
77
79
|
EntityEditScreenDefinition,
|
|
78
80
|
EntityListScreenDefinition,
|
|
79
81
|
FieldCondition,
|
|
@@ -100,7 +102,10 @@ export type {
|
|
|
100
102
|
export type { TargetRef } from "../engine/types/target-ref";
|
|
101
103
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node";
|
|
102
104
|
export type { WorkspaceDefinition } from "../engine/types/workspace";
|
|
103
|
-
export {
|
|
105
|
+
export {
|
|
106
|
+
PROJECTION_DETAIL_ENTITY,
|
|
107
|
+
WRITE_FORM_SECTION_ENTITY,
|
|
108
|
+
} from "../i18n/required-surface-keys";
|
|
104
109
|
export type {
|
|
105
110
|
AppSchema,
|
|
106
111
|
FeatureSchema,
|