@cosmicdrift/kumiko-framework 0.356.0 → 0.357.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/dist/db/event-store-executor-context.d.ts +3 -1
- package/dist/db/event-store-executor-context.js +3 -2
- package/dist/db/event-store-executor-write.js +3 -1
- package/dist/engine/boot-validator/card-meta-overflow.d.ts +10 -0
- package/dist/engine/boot-validator/card-meta-overflow.js +95 -0
- package/dist/engine/boot-validator/entity-handler.js +3 -2
- package/dist/engine/boot-validator/index.d.ts +1 -0
- package/dist/engine/boot-validator/index.js +4 -1
- package/dist/engine/boot-validator/query-refs.js +3 -0
- package/dist/engine/boot-validator/screens.js +5 -0
- package/dist/engine/boot-validator.d.ts +1 -1
- package/dist/engine/boot-validator.js +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/files/file-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/i18n/required-surface-keys.js +1 -0
- package/dist/pipeline/system-hooks.js +7 -3
- package/dist/testing/shared-entities.d.ts +10 -0
- package/dist/ui-types/card-column-roles.d.ts +14 -0
- package/dist/ui-types/card-column-roles.js +9 -0
- package/dist/ui-types/index.d.ts +2 -0
- package/dist/ui-types/index.js +1 -0
- package/package.json +4 -4
- package/src/changes.json +11 -0
|
@@ -29,7 +29,9 @@ export type ExecutorContext = {
|
|
|
29
29
|
readonly streamTenantFor: (user: SessionUser, requestedStreamTenantId?: TenantId) => TenantId;
|
|
30
30
|
readonly streamTenantOverrideFailure: (db: TenantDb, row: Record<string, unknown>, requestedStreamTenantId: TenantId | undefined) => WriteFailure | undefined;
|
|
31
31
|
readonly idFilter: (id: EntityId) => WhereObject;
|
|
32
|
-
readonly loadById: (id: EntityId, db: TenantDb
|
|
32
|
+
readonly loadById: (id: EntityId, db: TenantDb, options?: {
|
|
33
|
+
readonly includeDeleted?: boolean;
|
|
34
|
+
}) => Promise<Record<string, unknown> | null>;
|
|
33
35
|
readonly assertStreamWritable: (db: TenantDb, id: EntityId, tenantId: TenantId) => Promise<void>;
|
|
34
36
|
readonly loadWithOwnership: (db: TenantDb, idWhere: WhereObject, ownership: {
|
|
35
37
|
kind: "pass";
|
|
@@ -272,8 +272,9 @@ export function buildExecutorContext(table, entity, options) {
|
|
|
272
272
|
filter["isDeleted"] = false;
|
|
273
273
|
return filter;
|
|
274
274
|
}
|
|
275
|
-
async function loadById(id, db) {
|
|
276
|
-
|
|
275
|
+
async function loadById(id, db, options) {
|
|
276
|
+
// Only the soft-delete predicate is relaxed; the tenant filter of db.fetchOne stays.
|
|
277
|
+
const row = await db.fetchOne(table, options?.includeDeleted ? { id } : idFilter(id));
|
|
277
278
|
if (!row)
|
|
278
279
|
return null;
|
|
279
280
|
return await decryptForRead(rehydrateCompoundTypes(row, entity));
|
|
@@ -343,7 +343,9 @@ export function createWriteVerbs(ctx) {
|
|
|
343
343
|
"be SYSTEM_TENANT_ID or omitted.",
|
|
344
344
|
});
|
|
345
345
|
}
|
|
346
|
-
const previous = await loadById(payload.id, db
|
|
346
|
+
const previous = await loadById(payload.id, db, {
|
|
347
|
+
includeDeleted: updateOptions?.includeDeleted === true,
|
|
348
|
+
});
|
|
347
349
|
if (!previous)
|
|
348
350
|
return writeFailure(new NotFoundError(entityName, payload.id));
|
|
349
351
|
const streamTenantId = updateOptions?.streamTenantId;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FeatureDefinition } from "../types/index.js";
|
|
2
|
+
export type CardMetaOverflow = {
|
|
3
|
+
readonly feature: string;
|
|
4
|
+
readonly screen: string;
|
|
5
|
+
readonly location?: string;
|
|
6
|
+
readonly count: number;
|
|
7
|
+
readonly fields: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function collectCardMetaOverflow(features: readonly FeatureDefinition[]): readonly CardMetaOverflow[];
|
|
10
|
+
export declare function validateCardMetaOverflow(features: readonly FeatureDefinition[]): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CARD_META_MAX, cardColumnRoles } from "../../ui-types/card-column-roles.js";
|
|
2
|
+
import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta.js";
|
|
3
|
+
import { parseRefTarget } from "../parse-ref-target.js";
|
|
4
|
+
import { normalizeListColumn } from "../screen-helpers.js";
|
|
5
|
+
// Mirrors the type resolution of headless computeListViewModel; only the
|
|
6
|
+
// "select" outcome matters for the card status role.
|
|
7
|
+
function resolveColumnType(spec, storedType, entity) {
|
|
8
|
+
const col = normalizeListColumn(spec);
|
|
9
|
+
if (col.refEntity !== undefined && (storedType === undefined || storedType === "text")) {
|
|
10
|
+
return "reference";
|
|
11
|
+
}
|
|
12
|
+
if (storedType !== undefined)
|
|
13
|
+
return storedType;
|
|
14
|
+
const derived = entity?.derivedFields?.[col.field];
|
|
15
|
+
if (derived !== undefined)
|
|
16
|
+
return derived.valueType;
|
|
17
|
+
return LIST_ROW_META_COLUMNS[col.field] ?? "text";
|
|
18
|
+
}
|
|
19
|
+
function countOverflow(feature, screen, location, columns, storedTypeOf, entity) {
|
|
20
|
+
const cardColumns = columns.map((spec) => {
|
|
21
|
+
const col = normalizeListColumn(spec);
|
|
22
|
+
return {
|
|
23
|
+
field: col.field,
|
|
24
|
+
type: resolveColumnType(spec, storedTypeOf(col.field), entity),
|
|
25
|
+
renderer: col.renderer,
|
|
26
|
+
hideOnNarrow: col.hideOnNarrow,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const { meta } = cardColumnRoles(cardColumns);
|
|
30
|
+
if (meta.length <= CARD_META_MAX)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
feature: feature.name,
|
|
34
|
+
screen,
|
|
35
|
+
...(location !== undefined && { location }),
|
|
36
|
+
count: meta.length,
|
|
37
|
+
fields: meta.map((col) => col.field),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function relatedListOverflow(features, feature, screen, location, section) {
|
|
41
|
+
let source;
|
|
42
|
+
if (section.entity !== undefined) {
|
|
43
|
+
const target = parseRefTarget(section.entity, feature.name);
|
|
44
|
+
source = features.find((f) => f.name === target.featureName)?.entities?.[target.entityName];
|
|
45
|
+
}
|
|
46
|
+
const synthesizedTypes = new Map(section.columns.map((spec) => {
|
|
47
|
+
const col = normalizeListColumn(spec);
|
|
48
|
+
return [col.field, col.valueType ?? "text"];
|
|
49
|
+
}));
|
|
50
|
+
return countOverflow(feature, screen, location, section.columns, (field) => source?.fields[field]?.type ?? synthesizedTypes.get(field), source);
|
|
51
|
+
}
|
|
52
|
+
// Every declaration whose columns the narrow layout draws as a DataTable card.
|
|
53
|
+
// Dashboard list panels have their own table and are not covered.
|
|
54
|
+
export function collectCardMetaOverflow(features) {
|
|
55
|
+
const found = [];
|
|
56
|
+
const add = (overflow) => {
|
|
57
|
+
if (overflow !== undefined)
|
|
58
|
+
found.push(overflow);
|
|
59
|
+
};
|
|
60
|
+
for (const feature of features) {
|
|
61
|
+
for (const screen of Object.values(feature.screens)) {
|
|
62
|
+
if (screen.type === "entityList") {
|
|
63
|
+
const entity = feature.entities?.[screen.entity];
|
|
64
|
+
add(countOverflow(feature, screen.id, undefined, screen.columns, (field) => entity?.fields[field]?.type, entity));
|
|
65
|
+
if (screen.expandableRow !== undefined) {
|
|
66
|
+
add(relatedListOverflow(features, feature, screen.id, `expandableRow "${screen.expandableRow.title}"`, screen.expandableRow));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (screen.type === "projectionList") {
|
|
70
|
+
// The projection shim types every column "text"; only refEntity changes it.
|
|
71
|
+
add(countOverflow(feature, screen.id, undefined, screen.columns, () => "text", undefined));
|
|
72
|
+
}
|
|
73
|
+
else if (screen.type === "projectionDetail") {
|
|
74
|
+
const isTabs = screen.layout.mode === "tabs";
|
|
75
|
+
for (const section of screen.layout.sections) {
|
|
76
|
+
if (!("kind" in section) || section.kind !== "relatedList")
|
|
77
|
+
continue;
|
|
78
|
+
const location = isTabs
|
|
79
|
+
? `tab "${section.id ?? section.title}" section "${section.title}"`
|
|
80
|
+
: `section "${section.title}"`;
|
|
81
|
+
add(relatedListOverflow(features, feature, screen.id, location, section));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
export function validateCardMetaOverflow(features) {
|
|
89
|
+
const overflows = collectCardMetaOverflow(features);
|
|
90
|
+
// skip: nothing overflows, so there is nothing to report
|
|
91
|
+
if (overflows.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const lines = overflows.map((o) => ` - Feature "${o.feature}" screen "${o.screen}"${o.location !== undefined ? ` ${o.location}` : ""}: ${o.count} meta columns (${o.fields.join(", ")})`);
|
|
94
|
+
throw new Error(`List cards show at most ${CARD_META_MAX} meta columns (all columns except title, status select and hideOnNarrow ones); more would be cut off on narrow screens:\n${lines.join("\n")}\nReduce each to ${CARD_META_MAX} columns or mark the others hideOnNarrow: true.`);
|
|
95
|
+
}
|
|
@@ -710,11 +710,12 @@ function assertNoSelectOptionsQuery(field, path, featureName) {
|
|
|
710
710
|
export function validateEntitySelectAvailabilityQuery(feature, queryHandlers) {
|
|
711
711
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
712
712
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
713
|
-
if (field.type !== "select"
|
|
713
|
+
if ((field.type !== "select" && field.type !== "multiSelect") ||
|
|
714
|
+
field.optionsAvailabilityQuery === undefined)
|
|
714
715
|
continue;
|
|
715
716
|
const query = field.optionsAvailabilityQuery;
|
|
716
717
|
if (query.length === 0 || !queryHandlers.has(query)) {
|
|
717
|
-
throw new Error(`[Feature ${feature.name}] Select field "${fieldName}" on entity "${entityName}" ` +
|
|
718
|
+
throw new Error(`[Feature ${feature.name}] ${field.type === "select" ? "Select" : "MultiSelect"} field "${fieldName}" on entity "${entityName}" ` +
|
|
718
719
|
`declares optionsAvailabilityQuery "${query}" which is not a registered query-handler. ` +
|
|
719
720
|
`Check the QN spelling (expected "<feature>:query:<short>") and that the handler ` +
|
|
720
721
|
`is declared via r.queryHandler(...).`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnvelopeCipher } from "../../secrets/envelope-cipher.js";
|
|
2
2
|
import type { FeatureDefinition } from "../types/index.js";
|
|
3
|
+
export { type CardMetaOverflow, collectCardMetaOverflow } from "./card-meta-overflow.js";
|
|
3
4
|
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
4
5
|
export { collectWriteHandlerQns } from "./nav.js";
|
|
5
6
|
export { SECURITY_BASELINE_FEATURE_NAMES } from "./security-baseline.js";
|
|
@@ -7,6 +7,7 @@ import { warnOnUniqueAccessRoles } from "./access-roles.js";
|
|
|
7
7
|
import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
|
|
8
8
|
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.js";
|
|
9
9
|
import { validateFeatureBootChecks } from "./boot-check.js";
|
|
10
|
+
import { validateCardMetaOverflow } from "./card-meta-overflow.js";
|
|
10
11
|
import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, validateConfigReads, warnOnToggleableDependencies, } from "./config-deps.js";
|
|
11
12
|
import { collectDetailForScreens } from "./detail-screens.js";
|
|
12
13
|
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectAvailabilityQuery, validateEntitySelectConditionalOptions, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
|
|
@@ -29,10 +30,11 @@ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
|
|
|
29
30
|
import { validateTransferGraph } from "./transfer-graph.js";
|
|
30
31
|
import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
|
|
31
32
|
import { collectWorkspaceQns, resolveNavAllowlist, validateDefaultWorkspaceUniqueness, validateWorkspaces, } from "./workspaces.js";
|
|
32
|
-
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
33
33
|
// Re-export: wird von run-dev-app.ts benötigt um Write-Handler-QNs
|
|
34
34
|
// an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
|
|
35
35
|
// dieselbe Extraktionslogik.
|
|
36
|
+
export { collectCardMetaOverflow } from "./card-meta-overflow.js";
|
|
37
|
+
export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
36
38
|
export { collectWriteHandlerQns } from "./nav.js";
|
|
37
39
|
export { SECURITY_BASELINE_FEATURE_NAMES } from "./security-baseline.js";
|
|
38
40
|
export { MAX_TRANSFER_DEPTH } from "./transfer-graph.js";
|
|
@@ -217,6 +219,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
217
219
|
validateI18nSurfaceKeys(features);
|
|
218
220
|
validateEntityListScreens(features);
|
|
219
221
|
validateSubmitPrefilledHiddenFields(features);
|
|
222
|
+
validateCardMetaOverflow(features);
|
|
220
223
|
// Must run before validateProjectionListScreens: an unresolvable query
|
|
221
224
|
// there is silently treated as "capability absent" and surfaces as a
|
|
222
225
|
// misleading "no search parameter in its Zod schema" error instead of
|
|
@@ -45,6 +45,9 @@ function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
45
45
|
}
|
|
46
46
|
function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenType, fields) {
|
|
47
47
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
48
|
+
if (field.type === "multiSelect" && field.optionsAvailabilityQuery !== undefined) {
|
|
49
|
+
checkQueryRef(queryHandlers, field.optionsAvailabilityQuery, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) multiSelect field "${fieldName}" optionsAvailabilityQuery`);
|
|
50
|
+
}
|
|
48
51
|
if (field.type !== "select")
|
|
49
52
|
continue;
|
|
50
53
|
if (field.optionsQuery !== undefined) {
|
|
@@ -550,6 +550,11 @@ function validateFormSelectOptions(featureName, screenId, context, fieldName, fd
|
|
|
550
550
|
throw new Error(`${where} has optionsQueryPayload without optionsQuery`);
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
|
+
else if (fdef.type === "multiSelect" && fdef.optionsAvailabilityQuery !== undefined) {
|
|
554
|
+
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
555
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) multiSelect field "${fieldName}" has an empty optionsAvailabilityQuery`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
553
558
|
}
|
|
554
559
|
// Fail-closed currency-source gate (fw#2839), the runtime half of the
|
|
555
560
|
// narrowed `FormFieldDefinition` — an untyped JS consumer has no compiler to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator/index.js";
|
|
1
|
+
export { type CardMetaOverflow, collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator/index.js";
|
|
1
|
+
export { collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator/index.js";
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { hasAccess } from "./access.js";
|
|
|
3
3
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalProfile, type PrincipalStatus, type PrincipalStatusPlugin, TENANT_TEARDOWN_STATUSES, type TenantLifecycleStatusPlugin, } from "./active-membership.js";
|
|
4
4
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
5
5
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
6
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
6
|
+
export { type CardMetaOverflow, collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
7
7
|
export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
|
|
8
8
|
export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
|
|
9
9
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { hasAccess } from "./access.js";
|
|
|
4
4
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN_STATUSES, } from "./active-membership.js";
|
|
5
5
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
6
6
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
7
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
7
|
+
export { collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
8
8
|
export { buildAppSchema } from "./build-app-schema.js";
|
|
9
9
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
|
10
10
|
export { buildTarget } from "./build-target.js";
|
|
@@ -4,6 +4,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -82,6 +85,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
82
85
|
readonly description?: string;
|
|
83
86
|
readonly maxLength?: number;
|
|
84
87
|
readonly minLength?: number;
|
|
88
|
+
readonly autoComplete?: string;
|
|
85
89
|
readonly required?: boolean;
|
|
86
90
|
readonly searchable?: boolean;
|
|
87
91
|
readonly sortable?: boolean;
|
|
@@ -104,6 +108,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
104
108
|
readonly description?: string;
|
|
105
109
|
readonly maxLength?: number;
|
|
106
110
|
readonly minLength?: number;
|
|
111
|
+
readonly autoComplete?: string;
|
|
107
112
|
readonly required?: boolean;
|
|
108
113
|
readonly searchable?: boolean;
|
|
109
114
|
readonly sortable?: boolean;
|
|
@@ -126,6 +131,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
126
131
|
readonly description?: string;
|
|
127
132
|
readonly maxLength?: number;
|
|
128
133
|
readonly minLength?: number;
|
|
134
|
+
readonly autoComplete?: string;
|
|
129
135
|
readonly required?: boolean;
|
|
130
136
|
readonly searchable?: boolean;
|
|
131
137
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -82,6 +85,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
82
85
|
readonly description?: string;
|
|
83
86
|
readonly maxLength?: number;
|
|
84
87
|
readonly minLength?: number;
|
|
88
|
+
readonly autoComplete?: string;
|
|
85
89
|
readonly required?: boolean;
|
|
86
90
|
readonly searchable?: boolean;
|
|
87
91
|
readonly sortable?: boolean;
|
|
@@ -104,6 +108,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
104
108
|
readonly description?: string;
|
|
105
109
|
readonly maxLength?: number;
|
|
106
110
|
readonly minLength?: number;
|
|
111
|
+
readonly autoComplete?: string;
|
|
107
112
|
readonly required?: boolean;
|
|
108
113
|
readonly searchable?: boolean;
|
|
109
114
|
readonly sortable?: boolean;
|
|
@@ -126,6 +131,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
126
131
|
readonly description?: string;
|
|
127
132
|
readonly maxLength?: number;
|
|
128
133
|
readonly minLength?: number;
|
|
134
|
+
readonly autoComplete?: string;
|
|
129
135
|
readonly required?: boolean;
|
|
130
136
|
readonly searchable?: boolean;
|
|
131
137
|
readonly sortable?: boolean;
|
|
@@ -283,6 +283,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
283
283
|
pushKey(out, mint.reveal.title);
|
|
284
284
|
pushKey(out, mint.reveal.warning);
|
|
285
285
|
pushKey(out, mint.reveal.confirmLabel);
|
|
286
|
+
pushKey(out, mint.reveal.acknowledge);
|
|
286
287
|
for (const revealField of mint.reveal.fields) {
|
|
287
288
|
pushKey(out, revealField.label);
|
|
288
289
|
}
|
|
@@ -37,15 +37,19 @@ import { InternalError } from "../errors/index.js";
|
|
|
37
37
|
// call per tenant instead of one round-trip per event; handler stays the
|
|
38
38
|
// per-event fallback the dispatcher falls back to when a batch throws.
|
|
39
39
|
export const SEARCH_CONSUMER_NAME = "system:consumer:search";
|
|
40
|
+
// An executor update with includeDeleted leaves the row deleted; indexing it would make it findable again.
|
|
41
|
+
function isUpdateOfSoftDeletedRow(event, verb) {
|
|
42
|
+
return (verb === "updated" && reconstructStateForSearch(event.payload, "updated")["isDeleted"] === true);
|
|
43
|
+
}
|
|
40
44
|
// Resolves what one event implies for the search index, without touching
|
|
41
45
|
// the adapter — same decision tree the old per-event handler ran inline.
|
|
42
46
|
async function resolveSearchOperation(event, ctx, registry) {
|
|
43
47
|
const entityName = event.aggregateType;
|
|
44
48
|
const verb = event.type.split(".").pop();
|
|
45
49
|
const tenantId = event.tenantId;
|
|
46
|
-
// skip: delete/forgotten
|
|
47
|
-
// makes sense for created/updated/restored
|
|
48
|
-
if (verb === "deleted" || verb === "forgotten") {
|
|
50
|
+
// skip: delete/forgotten (and an update on a still soft-deleted row) remove the
|
|
51
|
+
// index entry — reconstruct only makes sense for live created/updated/restored.
|
|
52
|
+
if (verb === "deleted" || verb === "forgotten" || isUpdateOfSoftDeletedRow(event, verb)) {
|
|
49
53
|
return { kind: "remove", tenantId, entityType: entityName, entityId: event.aggregateId };
|
|
50
54
|
}
|
|
51
55
|
let state;
|
|
@@ -4,6 +4,7 @@ export declare const sharedWidgetEntity: import("@cosmicdrift/kumiko-types/field
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -28,6 +29,7 @@ export declare const sharedWidgetTable: import("../db/table-builder.js").EntityT
|
|
|
28
29
|
readonly description?: string;
|
|
29
30
|
readonly maxLength?: number;
|
|
30
31
|
readonly minLength?: number;
|
|
32
|
+
readonly autoComplete?: string;
|
|
31
33
|
readonly required?: boolean;
|
|
32
34
|
readonly searchable?: boolean;
|
|
33
35
|
readonly sortable?: boolean;
|
|
@@ -52,6 +54,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
52
54
|
readonly description?: string;
|
|
53
55
|
readonly maxLength?: number;
|
|
54
56
|
readonly minLength?: number;
|
|
57
|
+
readonly autoComplete?: string;
|
|
55
58
|
readonly required?: boolean;
|
|
56
59
|
readonly searchable?: boolean;
|
|
57
60
|
readonly sortable?: boolean;
|
|
@@ -74,6 +77,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
74
77
|
readonly description?: string;
|
|
75
78
|
readonly maxLength?: number;
|
|
76
79
|
readonly minLength?: number;
|
|
80
|
+
readonly autoComplete?: string;
|
|
77
81
|
readonly required?: boolean;
|
|
78
82
|
readonly searchable?: boolean;
|
|
79
83
|
readonly sortable?: boolean;
|
|
@@ -96,6 +100,7 @@ export declare const sharedUserEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
96
100
|
readonly description?: string;
|
|
97
101
|
readonly maxLength?: number;
|
|
98
102
|
readonly minLength?: number;
|
|
103
|
+
readonly autoComplete?: string;
|
|
99
104
|
readonly required?: boolean;
|
|
100
105
|
readonly searchable?: boolean;
|
|
101
106
|
readonly sortable?: boolean;
|
|
@@ -123,6 +128,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
123
128
|
readonly description?: string;
|
|
124
129
|
readonly maxLength?: number;
|
|
125
130
|
readonly minLength?: number;
|
|
131
|
+
readonly autoComplete?: string;
|
|
126
132
|
readonly required?: boolean;
|
|
127
133
|
readonly searchable?: boolean;
|
|
128
134
|
readonly sortable?: boolean;
|
|
@@ -145,6 +151,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
145
151
|
readonly description?: string;
|
|
146
152
|
readonly maxLength?: number;
|
|
147
153
|
readonly minLength?: number;
|
|
154
|
+
readonly autoComplete?: string;
|
|
148
155
|
readonly required?: boolean;
|
|
149
156
|
readonly searchable?: boolean;
|
|
150
157
|
readonly sortable?: boolean;
|
|
@@ -167,6 +174,7 @@ export declare const sharedUserTable: import("../db/table-builder.js").EntityTab
|
|
|
167
174
|
readonly description?: string;
|
|
168
175
|
readonly maxLength?: number;
|
|
169
176
|
readonly minLength?: number;
|
|
177
|
+
readonly autoComplete?: string;
|
|
170
178
|
readonly required?: boolean;
|
|
171
179
|
readonly searchable?: boolean;
|
|
172
180
|
readonly sortable?: boolean;
|
|
@@ -194,6 +202,7 @@ export declare const sharedItemEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
194
202
|
readonly description?: string;
|
|
195
203
|
readonly maxLength?: number;
|
|
196
204
|
readonly minLength?: number;
|
|
205
|
+
readonly autoComplete?: string;
|
|
197
206
|
readonly required?: boolean;
|
|
198
207
|
readonly searchable?: boolean;
|
|
199
208
|
readonly sortable?: boolean;
|
|
@@ -237,6 +246,7 @@ export declare const sharedItemTable: import("../db/table-builder.js").EntityTab
|
|
|
237
246
|
readonly description?: string;
|
|
238
247
|
readonly maxLength?: number;
|
|
239
248
|
readonly minLength?: number;
|
|
249
|
+
readonly autoComplete?: string;
|
|
240
250
|
readonly required?: boolean;
|
|
241
251
|
readonly searchable?: boolean;
|
|
242
252
|
readonly sortable?: boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const CARD_META_MAX = 3;
|
|
2
|
+
export type CardRoleColumn = {
|
|
3
|
+
readonly field: string;
|
|
4
|
+
readonly highlighted?: boolean;
|
|
5
|
+
readonly type?: string;
|
|
6
|
+
readonly renderer?: unknown;
|
|
7
|
+
readonly hideOnNarrow?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type CardColumnRoles<C extends CardRoleColumn> = {
|
|
10
|
+
readonly title: C | undefined;
|
|
11
|
+
readonly status: C | undefined;
|
|
12
|
+
readonly meta: readonly C[];
|
|
13
|
+
};
|
|
14
|
+
export declare function cardColumnRoles<C extends CardRoleColumn>(columns: readonly C[]): CardColumnRoles<C>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const CARD_META_MAX = 3;
|
|
2
|
+
// One rule for the narrow-layout list card, shared by the renderer and the
|
|
3
|
+
// boot validator so the validator counts exactly what the card would draw.
|
|
4
|
+
export function cardColumnRoles(columns) {
|
|
5
|
+
const title = columns.find((col) => col.highlighted === true) ?? columns[0];
|
|
6
|
+
const status = columns.find((col) => col !== title && col.type === "select" && col.renderer === undefined);
|
|
7
|
+
const meta = columns.filter((col) => col !== title && col !== status && col.hideOnNarrow !== true);
|
|
8
|
+
return { title, status, meta };
|
|
9
|
+
}
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-n
|
|
|
16
16
|
export type { WorkspaceDefinition } from "../engine/types/workspace.js";
|
|
17
17
|
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
18
18
|
export type { AppSchema, FeatureSchema, QualifiedContentCollection, WorkspaceSchema, } from "./app-schema.js";
|
|
19
|
+
export type { CardColumnRoles, CardRoleColumn } from "./card-column-roles.js";
|
|
20
|
+
export { CARD_META_MAX, cardColumnRoles } from "./card-column-roles.js";
|
|
19
21
|
export { type Formality, formalLocaleTag } from "./formality.js";
|
|
20
22
|
export type { ListRowMetaColumnType, ListRowMetaReference, ReferenceLookupSource, SystemReferenceLabel, } from "./list-row-meta.js";
|
|
21
23
|
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findU
|
|
|
26
26
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
27
27
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
28
28
|
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
29
|
+
export { CARD_META_MAX, cardColumnRoles } from "./card-column-roles.js";
|
|
29
30
|
export { formalLocaleTag } from "./formality.js";
|
|
30
31
|
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|
|
31
32
|
export { resolveTranslationValue, translationValueOtherText, } from "./plural.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.357.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -210,8 +210,8 @@
|
|
|
210
210
|
"./package.json": "./package.json"
|
|
211
211
|
},
|
|
212
212
|
"dependencies": {
|
|
213
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
214
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-http": "0.357.0",
|
|
214
|
+
"@cosmicdrift/kumiko-types": "0.357.0",
|
|
215
215
|
"bullmq": "^5.76.7",
|
|
216
216
|
"bun-types": "^1.3.13",
|
|
217
217
|
"hono": "^4.13.1",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
"zod": "^4.4.3"
|
|
228
228
|
},
|
|
229
229
|
"devDependencies": {
|
|
230
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
230
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.357.0",
|
|
231
231
|
"bun-types": "^1.3.13",
|
|
232
232
|
"pino-pretty": "^13.1.3"
|
|
233
233
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.357.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Executor update accepts includeDeleted for soft-deleted rows",
|
|
6
|
+
"detail": "`executor.update(payload, user, db, { includeDeleted: true })` loads the row without the soft-delete filter (the tenant filter stays), so server code can move a trashed row together with its parent (for example a team change) and a later restore lands consistently. The row stays deleted. The default is unchanged (NotFound) and the generic HTTP update handler never sets the option. The projection apply was already unfiltered by `isDeleted`, so live and rebuild agree. The search consumer treats an update on a soft-deleted row as a removal, so the moved row does not reappear in search."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.357.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Boot validation covers multiSelect optionsAvailabilityQuery on entity and screen fields"
|
|
12
|
+
},
|
|
2
13
|
{
|
|
3
14
|
"version": "0.356.0",
|
|
4
15
|
"type": "improvement",
|