@cosmicdrift/kumiko-framework 0.377.0 → 0.379.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/AGENTS.md +4 -1
- package/agents/examples/compose-stacks.md +1 -2
- package/agents/examples/currencies-global.md +2 -3
- package/agents/examples/delivery-notifications.md +25 -2
- package/agents/examples/embedded.md +177 -0
- package/agents/examples/event-sourcing.md +6 -5
- package/agents/examples/legal-pages.md +5 -5
- package/agents/examples/reference-data.md +26 -21
- package/agents/examples/relations.md +2 -1
- package/agents/features.md +5 -5
- package/agents/widgets.md +1 -0
- package/dist/api/entity-read-access.d.ts +2 -0
- package/dist/api/entity-read-access.js +27 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/server.js +10 -3
- package/dist/api/sse-route.d.ts +5 -0
- package/dist/api/sse-route.js +59 -9
- package/dist/db/entity-table-from-registry.d.ts +2 -1
- package/dist/db/entity-table-meta.d.ts +0 -2
- package/dist/db/entity-table-meta.js +1 -3
- package/dist/db/event-store-executor-write.js +21 -16
- package/dist/db/executor-managed-tables.d.ts +6 -0
- package/dist/db/executor-managed-tables.js +40 -0
- package/dist/db/index.d.ts +4 -2
- package/dist/db/index.js +4 -2
- package/dist/db/money.d.ts +3 -2
- package/dist/db/money.js +2 -7
- package/dist/db/queries/backfill-pii.js +22 -12
- package/dist/db/queries/database-now.d.ts +5 -0
- package/dist/db/queries/database-now.js +12 -0
- package/dist/db/queries/event-store-orphan-rows.d.ts +8 -0
- package/dist/db/queries/event-store-orphan-rows.js +32 -0
- package/dist/db/query.js +28 -0
- package/dist/db/reference-data.d.ts +5 -4
- package/dist/db/reference-data.js +23 -21
- package/dist/db/table-builder.js +2 -0
- package/dist/db/tenant-db-runner.d.ts +2 -0
- package/dist/db/tenant-db-runner.js +7 -0
- package/dist/db/tenant-db.d.ts +3 -0
- package/dist/db/tenant-db.js +54 -18
- package/dist/engine/boot-validator/entity-handler.js +12 -4
- package/dist/engine/boot-validator/form-derivations.d.ts +4 -0
- package/dist/engine/boot-validator/form-derivations.js +82 -0
- package/dist/engine/boot-validator/query-refs.js +8 -0
- package/dist/engine/boot-validator/screens.js +50 -12
- package/dist/engine/build-config-feature-schema.js +33 -10
- package/dist/engine/embedded-derived.d.ts +1 -1
- package/dist/engine/embedded-derived.js +7 -1
- package/dist/engine/feature-ui-extensions.js +4 -0
- package/dist/engine/form-derivations.d.ts +21 -0
- package/dist/engine/form-derivations.js +175 -0
- package/dist/engine/index.d.ts +4 -1
- package/dist/engine/index.js +2 -0
- package/dist/engine/minor-units.d.ts +2 -0
- package/dist/engine/minor-units.js +8 -0
- package/dist/engine/registry-state.js +2 -0
- package/dist/engine/registry-validate.js +4 -1
- package/dist/engine/schema-builder.js +18 -4
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +3 -0
- package/dist/engine/soft-delete-cleanup.js +53 -24
- package/dist/engine/steps/unsafe-projection-delete.js +2 -1
- package/dist/engine/template-variables.d.ts +2 -1
- package/dist/engine/template-variables.js +4 -0
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/with-form-derivations.d.ts +9 -0
- package/dist/engine/with-form-derivations.js +16 -0
- package/dist/entrypoint/index.d.ts +1 -3
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +4 -1
- package/dist/errors/serialize.d.ts +1 -0
- package/dist/errors/serialize.js +9 -5
- package/dist/errors/write-error-info.js +3 -2
- package/dist/errors/zod-bridge.js +7 -0
- package/dist/event-store/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- package/dist/files/action-form-upload.d.ts +16 -0
- package/dist/files/action-form-upload.js +45 -0
- package/dist/files/bind-pending-file-refs.d.ts +14 -0
- package/dist/files/bind-pending-file-refs.js +44 -4
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +41 -13
- package/dist/files/file-routes.js +29 -2
- package/dist/files/index.d.ts +2 -1
- package/dist/files/index.js +1 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +3 -2
- package/dist/files/purge-expired-file-ref-bytes.js +11 -5
- package/dist/i18n/settings-hub-keys.js +1 -0
- package/dist/internal/testing.d.ts +2 -0
- package/dist/internal/testing.js +15 -1
- package/dist/migrations/pii-event-backfill.js +14 -7
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +14 -3
- package/dist/pipeline/cascade-handler.js +79 -10
- package/dist/pipeline/dispatch-shared.js +1 -9
- package/dist/pipeline/msp-rebuild.js +5 -3
- package/dist/pipeline/projection-rebuild.js +4 -2
- package/dist/pipeline/projections-runner.js +2 -1
- package/dist/rate-limit/index.d.ts +1 -1
- package/dist/rate-limit/index.js +1 -1
- package/dist/rate-limit/middleware.d.ts +4 -0
- package/dist/rate-limit/middleware.js +5 -2
- package/dist/search/configure-tenant-index.d.ts +4 -0
- package/dist/search/configure-tenant-index.js +8 -0
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.js +1 -0
- package/dist/ui-types/index.d.ts +4 -2
- package/dist/ui-types/index.js +3 -2
- package/dist/ui-types/list-row-meta.d.ts +3 -0
- package/dist/ui-types/list-row-meta.js +12 -1
- package/kumiko-es-tables.json +6 -0
- package/package.json +4 -3
- package/src/changes.json +142 -0
- package/src/scripts/codemod/README.md +20 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +416 -0
- package/src/scripts/codemod/package-merge.ts +0 -11
|
@@ -18,6 +18,7 @@ import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js"
|
|
|
18
18
|
import { explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
19
19
|
import { metricField } from "../types/index.js";
|
|
20
20
|
import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
|
|
21
|
+
import { validateFormDerivations, validateFormDerivationsEnforced } from "./form-derivations.js";
|
|
21
22
|
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
22
23
|
function isRowClickAction(action) {
|
|
23
24
|
return (action.kind === "navigate" || action.kind === "drawer") && action.rowClick === true;
|
|
@@ -182,21 +183,25 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
182
183
|
return;
|
|
183
184
|
if (target.screen.type === "dashboard") {
|
|
184
185
|
const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
|
|
185
|
-
const filter = target.screen
|
|
186
|
-
|
|
186
|
+
const { filter, record, timeRange } = target.screen;
|
|
187
|
+
const readIds = [filter?.id, record?.param, timeRange?.id].filter((id) => id !== undefined);
|
|
188
|
+
if (readIds.length === 0) {
|
|
187
189
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
188
190
|
`params on navigate-target ${targetDescriptor} (dashboard) — target dashboard declares no ` +
|
|
189
|
-
`filter — params would be a no-op. Remove the params extractor, or add a \`filter\` to the ` +
|
|
191
|
+
`filter, time range or record binding — params would be a no-op. Remove the params extractor, or add a \`filter\` (or a \`record\` binding) to the ` +
|
|
190
192
|
`target dashboard so it has somewhere to read the value from.`);
|
|
191
193
|
}
|
|
192
194
|
const extractedKeys = rowFieldExtractorKeys(action.params);
|
|
193
|
-
if (!extractedKeys.includes(
|
|
195
|
+
if (!readIds.some((id) => extractedKeys.includes(id))) {
|
|
196
|
+
const readDescriptor = filter !== undefined && readIds.length === 1
|
|
197
|
+
? `filter id is "${filter.id}"`
|
|
198
|
+
: `URL params are [${readIds.join(", ")}]`;
|
|
194
199
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
|
|
195
200
|
`params [${extractedKeys.join(", ")}] on navigate-target ${targetDescriptor} (dashboard) whose ` +
|
|
196
|
-
|
|
197
|
-
`unset. Fix the params extractor to produce
|
|
201
|
+
`${readDescriptor} — none of the extracted keys match, so the value would stay ` +
|
|
202
|
+
`unset. Fix the params extractor to produce one of these keys, or remove it.`);
|
|
198
203
|
}
|
|
199
|
-
// skip:
|
|
204
|
+
// skip: a read id is present and the extractor's keys cover it — valid dashboard deep-link.
|
|
200
205
|
return;
|
|
201
206
|
}
|
|
202
207
|
if (!readsNavigateParamsAsFormPrefill(target.screen, action.entityId, screenEntity)) {
|
|
@@ -945,6 +950,17 @@ function validateInlineFormScreen(feature, screenId, screen, kind, allWriteHandl
|
|
|
945
950
|
const fieldNames = validateFormFieldsMap(feature.name, screenId, kind, screen.fields, allowEmptyMintForm);
|
|
946
951
|
validateFormLayoutSections(feature.name, screenId, kind, screen.layout, fieldNames, screen.fields, allowEmptyMintForm && fieldNames.size === 0);
|
|
947
952
|
validateWizardLayout(feature.name, screenId, kind, screen.layout, featureMap);
|
|
953
|
+
if (screen.type === "actionForm") {
|
|
954
|
+
validateFormDerivations(feature.name, screenId, screen);
|
|
955
|
+
validateFormDerivationsEnforced(feature.name, screenId, screen, featureMap);
|
|
956
|
+
}
|
|
957
|
+
if (screen.type === "actionForm" &&
|
|
958
|
+
screen.load?.singleton === true &&
|
|
959
|
+
screen.load.idParam !== undefined) {
|
|
960
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (actionForm) sets both load.singleton: true ` +
|
|
961
|
+
`and load.idParam — singleton means the query picks the row from the caller's context, so ` +
|
|
962
|
+
`there is no record id to send under idParam. Remove idParam.`);
|
|
963
|
+
}
|
|
948
964
|
validateInlineFormNavTargets(feature, screenId, screen, kind, lookups);
|
|
949
965
|
}
|
|
950
966
|
// SecretMintConfirmStep (fw#2838): an optional proof-of-receipt form rendered
|
|
@@ -1287,17 +1303,25 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1287
1303
|
else if (action.kind === "clientAction") {
|
|
1288
1304
|
validateClientActionName(feature.name, screenId, "projectionList", "rowAction", action);
|
|
1289
1305
|
}
|
|
1306
|
+
else if (!allWriteHandlerQns.has(action.handler)) {
|
|
1307
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) rowAction "${action.id}" ` +
|
|
1308
|
+
`handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
|
|
1309
|
+
`(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
1310
|
+
}
|
|
1290
1311
|
}
|
|
1291
1312
|
validateAtMostOneRowClick(feature.name, screenId, "projectionList", screen.rowActions);
|
|
1292
1313
|
}
|
|
1293
|
-
//
|
|
1294
|
-
// here — plain navigate (no tab) and writeHandler toolbarActions on
|
|
1295
|
-
// projectionList have no boot check yet (pre-existing gap).
|
|
1314
|
+
// Plain navigate toolbarActions (no tab) have no boot check yet.
|
|
1296
1315
|
if (screen.toolbarActions !== undefined) {
|
|
1297
1316
|
for (const action of screen.toolbarActions) {
|
|
1298
1317
|
if (action.kind === "drawer") {
|
|
1299
1318
|
validateDrawerTargetAction(feature.name, screenId, "projectionList", "toolbarAction", action, feature.screens);
|
|
1300
1319
|
}
|
|
1320
|
+
else if (action.kind === "writeHandler" && !allWriteHandlerQns.has(action.handler)) {
|
|
1321
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) toolbarAction "${action.id}" ` +
|
|
1322
|
+
`handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
|
|
1323
|
+
`(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
|
|
1324
|
+
}
|
|
1301
1325
|
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
1302
1326
|
resolveRowActionNavigateTarget(feature.name, screenId, "projectionList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
|
|
1303
1327
|
}
|
|
@@ -2066,7 +2090,7 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
2066
2090
|
validateDashboardScreenPanel(featureName, screenId, panel, featureMap);
|
|
2067
2091
|
}
|
|
2068
2092
|
else if (panel.kind === "text") {
|
|
2069
|
-
validateDashboardTextPanel(featureName, screenId, panel);
|
|
2093
|
+
validateDashboardTextPanel(featureName, screenId, panel, featureMap);
|
|
2070
2094
|
}
|
|
2071
2095
|
else {
|
|
2072
2096
|
validateDashboardQueryPanel(featureName, screenId, panel, featureMap);
|
|
@@ -2250,7 +2274,7 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
|
|
|
2250
2274
|
throw new Error(`${context} fallback "${panel.fallback}" has its own visibleWhen; a fallback must always render.`);
|
|
2251
2275
|
}
|
|
2252
2276
|
}
|
|
2253
|
-
function validateDashboardTextPanel(featureName, screenId, panel) {
|
|
2277
|
+
function validateDashboardTextPanel(featureName, screenId, panel, featureMap) {
|
|
2254
2278
|
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) text-panel "${panel.id}"`;
|
|
2255
2279
|
if (panel.blocks.length === 0) {
|
|
2256
2280
|
throw new Error(`${context} has an empty blocks list. Add at least one block.`);
|
|
@@ -2261,6 +2285,20 @@ function validateDashboardTextPanel(featureName, screenId, panel) {
|
|
|
2261
2285
|
if (block.kind === "list" && block.items.length === 0) {
|
|
2262
2286
|
throw new Error(`${context} has a list block without items. Add items or remove the block.`);
|
|
2263
2287
|
}
|
|
2288
|
+
if (block.kind === "link")
|
|
2289
|
+
validateDashboardTextLinkBlock(context, block, featureName, featureMap);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function validateDashboardTextLinkBlock(context, block, featureName, featureMap) {
|
|
2293
|
+
const targetQn = resolveScreenTargetQn(featureName, block.screen);
|
|
2294
|
+
const [targetFeatureName = "", , targetShortId = ""] = targetQn.split(":");
|
|
2295
|
+
if (featureMap.get(targetFeatureName)?.screens[targetShortId] === undefined) {
|
|
2296
|
+
throw new Error(`${context} link block screen "${block.screen}" does not resolve to a registered screen ` +
|
|
2297
|
+
`(checked "${targetQn}"). Use a same-feature short id or a cross-feature QN "<feature>:screen:<id>".`);
|
|
2298
|
+
}
|
|
2299
|
+
if (block.roles?.length === 0) {
|
|
2300
|
+
throw new Error(`${context} link block "${block.screen}" has an empty roles list, which would hide it from everyone. ` +
|
|
2301
|
+
`List the roles or omit roles.`);
|
|
2264
2302
|
}
|
|
2265
2303
|
}
|
|
2266
2304
|
function validateDashboardPanelVisibleWhen(context, visibleWhen) {
|
|
@@ -46,6 +46,10 @@ const PROVIDER_PANEL_TITLE_ENTRY = {
|
|
|
46
46
|
es: "Proveedor",
|
|
47
47
|
};
|
|
48
48
|
const PROVIDER_SECTION_TITLE_KEY = "config.settings.provider";
|
|
49
|
+
// The empty option of a provider select means "no own provider, the platform handles it". A selector
|
|
50
|
+
// names it through `<field label key>.platformDefault`, else this generic text applies.
|
|
51
|
+
const PLATFORM_DEFAULT_LABEL_SUFFIX = ".platformDefault";
|
|
52
|
+
const PLATFORM_DEFAULT_FALLBACK_KEY = "config.settings.provider.platformDefault";
|
|
49
53
|
const SECRETS_SECTION_DESCRIPTION_KEY = "config.secrets.description";
|
|
50
54
|
const audienceNavShortId = (scope) => `audience-${scope}`;
|
|
51
55
|
// Generated post-boot, never via r.nav() — the boot validator exempts exactly these QNs (an app references one to place the settings group inline).
|
|
@@ -214,8 +218,8 @@ function buildFeatureNav(scope, feature, ordered, access, declaredTranslationKey
|
|
|
214
218
|
access,
|
|
215
219
|
};
|
|
216
220
|
}
|
|
217
|
-
// One configEdit screen + child-nav per feature; a tenant-scope selector owner
|
|
218
|
-
//
|
|
221
|
+
// One configEdit screen + child-nav per feature; a tenant-scope selector owner folds its
|
|
222
|
+
// gated plugins into its own screen (a dashboard when it carries extra panels).
|
|
219
223
|
function buildFeatureScreensAndNavs(scope, visible, hub, gating, secretsAccess) {
|
|
220
224
|
const out = { screens: [], navs: [], translations: {} };
|
|
221
225
|
const ungated = visible.filter((v) => !gating.pluginFeatures.has(v.key.ownerFeature));
|
|
@@ -230,7 +234,7 @@ function buildFeatureScreensAndNavs(scope, visible, hub, gating, secretsAccess)
|
|
|
230
234
|
out.navs.push(buildFeatureNav(scope, feature, ordered, access, hub.declaredTranslationKeys));
|
|
231
235
|
continue;
|
|
232
236
|
}
|
|
233
|
-
const ownerHub =
|
|
237
|
+
const ownerHub = buildSelectorOwnerScreens(feature, ordered, access, plugins, gating.ownerPanelsByGroup.get(feature) ?? [], hub, secretsAccess);
|
|
234
238
|
out.screens.push(...ownerHub.screens);
|
|
235
239
|
out.navs.push(buildFeatureNav(scope, feature, ordered, ownerHub.access, hub.declaredTranslationKeys));
|
|
236
240
|
Object.assign(out.translations, ownerHub.translations);
|
|
@@ -251,15 +255,19 @@ function assertUniquePanelIds(ownerGroup, panels) {
|
|
|
251
255
|
// One form for the provider and the settings of every provider: a single save writes
|
|
252
256
|
// all of it, so switching sections can't drop typed input. Each plugin's fields are
|
|
253
257
|
// shown only while its provider is picked in the form; hidden fields are not saved.
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
// Without extra panels the form is the owner's screen itself; with panels it sits
|
|
259
|
+
// chromeless in a dashboard.
|
|
260
|
+
function buildSelectorOwnerScreens(ownerGroup, ownerKeys, ownerAccess, plugins, ownerPanels, hub, secretsAccess) {
|
|
261
|
+
const hasPanels = ownerPanels.length > 0;
|
|
262
|
+
const selectionScreenId = hasPanels ? `${ownerGroup}-tenant-selection` : `${ownerGroup}-tenant`;
|
|
256
263
|
const form = new ProviderFormBuilder(selectionScreenId, buildScreen(selectionScreenId, "tenant", ownerGroup, ownerKeys, ownerAccess, hub, {
|
|
257
264
|
description: EXTENSION_SELECTOR_HINT_KEY,
|
|
258
|
-
sectionTitle: PROVIDER_SECTION_TITLE_KEY,
|
|
265
|
+
...(hasPanels && { sectionTitle: PROVIDER_SECTION_TITLE_KEY }),
|
|
259
266
|
}));
|
|
260
267
|
const accessRules = [ownerAccess];
|
|
261
268
|
const translations = {};
|
|
262
|
-
|
|
269
|
+
if (hasPanels)
|
|
270
|
+
translations[`screen:${selectionScreenId}.title`] = PROVIDER_PANEL_TITLE_ENTRY;
|
|
263
271
|
Object.assign(translations, providerOptionTranslations(form.base, hub));
|
|
264
272
|
for (const plugin of plugins) {
|
|
265
273
|
const providerFieldId = form.fieldIdOfConfigKey(plugin.selectorQn);
|
|
@@ -279,7 +287,10 @@ function buildSelectorOwnerDashboard(ownerGroup, ownerKeys, ownerAccess, plugins
|
|
|
279
287
|
}
|
|
280
288
|
}
|
|
281
289
|
const access = unionAccessRules(accessRules);
|
|
282
|
-
const
|
|
290
|
+
const providerForm = form.build(access);
|
|
291
|
+
if (!hasPanels)
|
|
292
|
+
return { screens: [providerForm], access, translations };
|
|
293
|
+
const selectionScreen = { ...providerForm, dormant: true };
|
|
283
294
|
const dashboard = {
|
|
284
295
|
id: `${ownerGroup}-tenant`,
|
|
285
296
|
type: "dashboard",
|
|
@@ -726,6 +737,14 @@ function providerOptionTranslations(selectionScreen, hub) {
|
|
|
726
737
|
const featureByPluginId = hub.selectorOptionFeatures.get(qn);
|
|
727
738
|
if (featureByPluginId === undefined)
|
|
728
739
|
continue;
|
|
740
|
+
const fieldLabelTranslationKey = selectionScreen.fieldLabels?.[fieldId];
|
|
741
|
+
const platformDefaultLabel = (fieldLabelTranslationKey !== undefined
|
|
742
|
+
? hub.translationsByKey.get(`${fieldLabelTranslationKey}${PLATFORM_DEFAULT_LABEL_SUFFIX}`)
|
|
743
|
+
: undefined) ?? hub.translationsByKey.get(PLATFORM_DEFAULT_FALLBACK_KEY);
|
|
744
|
+
if (platformDefaultLabel !== undefined) {
|
|
745
|
+
out[`${fieldLabelKey(SETTINGS_HUB_FEATURE, CONFIG_EDIT_ENTITY, fieldId)}:option:`] =
|
|
746
|
+
platformDefaultLabel;
|
|
747
|
+
}
|
|
729
748
|
for (const [pluginId, feature] of featureByPluginId) {
|
|
730
749
|
const label = hub.translationsByKey.get(`${feature}.settings`);
|
|
731
750
|
if (label === undefined)
|
|
@@ -738,8 +757,12 @@ function providerOptionTranslations(selectionScreen, hub) {
|
|
|
738
757
|
}
|
|
739
758
|
function deriveField(key, selectorPluginIds, screenKeys, fieldIdOf) {
|
|
740
759
|
const def = key.def;
|
|
741
|
-
if (selectorPluginIds !== undefined)
|
|
742
|
-
return createSelectField({
|
|
760
|
+
if (selectorPluginIds !== undefined) {
|
|
761
|
+
return createSelectField({
|
|
762
|
+
options: selectorPluginIds.length > 0 ? ["", ...selectorPluginIds] : selectorPluginIds,
|
|
763
|
+
display: "radio",
|
|
764
|
+
});
|
|
765
|
+
}
|
|
743
766
|
switch (def.type) {
|
|
744
767
|
case "number":
|
|
745
768
|
return createNumberField({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EmbeddedDerivedCellDef, EmbeddedSubFieldDef } from "./types/index.ts";
|
|
2
2
|
/** Computes a derived cell from its source values. Missing/non-numeric
|
|
3
|
-
* sources: "multiply" with any missing source → undefined; "sum"/"subtract"
|
|
3
|
+
* sources: "multiply"/"percent" with any missing source → undefined; "sum"/"subtract"
|
|
4
4
|
* treat missing as 0 only when at least one source is present — if every
|
|
5
5
|
* source is missing, return undefined (do not invent 0). Money cells are
|
|
6
6
|
* minor-unit integers — unit-agnostic arithmetic; `withDerivedCells` rounds
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
2
|
/** Computes a derived cell from its source values. Missing/non-numeric
|
|
3
|
-
* sources: "multiply" with any missing source → undefined; "sum"/"subtract"
|
|
3
|
+
* sources: "multiply"/"percent" with any missing source → undefined; "sum"/"subtract"
|
|
4
4
|
* treat missing as 0 only when at least one source is present — if every
|
|
5
5
|
* source is missing, return undefined (do not invent 0). Money cells are
|
|
6
6
|
* minor-unit integers — unit-agnostic arithmetic; `withDerivedCells` rounds
|
|
@@ -15,6 +15,12 @@ export function computeDerivedCellValue(op, values) {
|
|
|
15
15
|
}
|
|
16
16
|
return product;
|
|
17
17
|
}
|
|
18
|
+
if (op === "percent") {
|
|
19
|
+
const [base, rate] = values;
|
|
20
|
+
if (values.length !== 2 || base === undefined || rate === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
return (base * rate) / 100;
|
|
23
|
+
}
|
|
18
24
|
if (values.every((value) => value === undefined))
|
|
19
25
|
return undefined;
|
|
20
26
|
const numeric = values.map((value) => value ?? 0);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { escapeHatchGrantsProblem } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
3
|
import { WEBSOCKET_MAX_CONNECTIONS_PER_USER_LIMIT, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, } from "@cosmicdrift/kumiko-types/websocket-route";
|
|
4
|
+
import { markExecutorManagedTable } from "../db/executor-managed-tables.js";
|
|
4
5
|
import { bindHookEscapeHatchGrant } from "../pipeline/system-identity-switch.js";
|
|
5
6
|
import { LifecycleHookTypes } from "./constants.js";
|
|
6
7
|
import { resolveName } from "./handler-helpers.js";
|
|
@@ -298,6 +299,9 @@ export function buildUiExtensionsMethods(state, name) {
|
|
|
298
299
|
throw new Error(`[Feature ${name}] Projection "${definition.name}" already registered. ` +
|
|
299
300
|
`Projection names must be unique per feature.`);
|
|
300
301
|
}
|
|
302
|
+
if (typeof definition.table === "object" && definition.table !== null) {
|
|
303
|
+
markExecutorManagedTable(definition.table);
|
|
304
|
+
}
|
|
301
305
|
state.projections[definition.name] = definition;
|
|
302
306
|
},
|
|
303
307
|
multiStreamProjection(definition) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormFieldDefinition } from "@cosmicdrift/kumiko-types/fields";
|
|
2
|
+
import type { FormFieldDerivation } from "./types/screen.ts";
|
|
3
|
+
export type FormDerivationScreen = {
|
|
4
|
+
readonly fields: Readonly<Record<string, FormFieldDefinition>>;
|
|
5
|
+
readonly derive?: Readonly<Record<string, FormFieldDerivation>>;
|
|
6
|
+
};
|
|
7
|
+
/** Targets ordered so every derived source is computed before its reader. */
|
|
8
|
+
export declare function orderFormDerivations(derive: Readonly<Record<string, FormFieldDerivation>>): readonly string[];
|
|
9
|
+
/** True when `field` is a `listSum` without `whenEmpty` whose list has no
|
|
10
|
+
* rows: the sum has nothing to add up, so the user types the value. */
|
|
11
|
+
export declare function isFormDerivationInputMode(screen: FormDerivationScreen, values: unknown, field: string): boolean;
|
|
12
|
+
/** True when `field` is only read as a `whenEmpty` operand of `listSum`
|
|
13
|
+
* derivations whose lists all have rows: nothing reads it, so it is locked
|
|
14
|
+
* rather than inviting an input that is silently dropped. */
|
|
15
|
+
export declare function isUnusedFallbackOperand(screen: FormDerivationScreen, values: unknown, field: string): boolean;
|
|
16
|
+
/** Recomputes every derived head field of an action form from its inputs.
|
|
17
|
+
* Pure and client-safe: the renderer runs it live, the server runs it
|
|
18
|
+
* again (`withFormDerivations`) and overwrites whatever the client sent.
|
|
19
|
+
* Money operands are converted to minor units for the arithmetic and money
|
|
20
|
+
* targets are written back as `{ amount, currency }` in major units. */
|
|
21
|
+
export declare function applyFormDerivations(values: unknown, screen: FormDerivationScreen): unknown;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { computeDerivedCellValue, roundDerivedCellValue, withDerivedCells, } from "./embedded-derived.js";
|
|
3
|
+
import { MINOR_UNIT_SCALE, toMinorUnits } from "./minor-units.js";
|
|
4
|
+
function readMoneyOperand(raw) {
|
|
5
|
+
if (typeof raw === "number")
|
|
6
|
+
return { minor: toMinorUnits(raw), currency: undefined };
|
|
7
|
+
if (!isPlainObject(raw) || typeof raw["amount"] !== "number")
|
|
8
|
+
return undefined;
|
|
9
|
+
const currency = raw["currency"];
|
|
10
|
+
return {
|
|
11
|
+
minor: toMinorUnits(raw["amount"]),
|
|
12
|
+
currency: typeof currency === "string" && currency !== "" ? currency : undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function readOperand(values, fields, name) {
|
|
16
|
+
const raw = values[name];
|
|
17
|
+
if (fields[name]?.type === "money") {
|
|
18
|
+
const money = readMoneyOperand(raw);
|
|
19
|
+
return { value: money?.minor, currency: money?.currency };
|
|
20
|
+
}
|
|
21
|
+
return { value: typeof raw === "number" ? raw : undefined, currency: undefined };
|
|
22
|
+
}
|
|
23
|
+
function dependenciesOf(derivation) {
|
|
24
|
+
if (derivation.op === "listSum") {
|
|
25
|
+
return derivation.whenEmpty === undefined ? [] : derivation.whenEmpty.from;
|
|
26
|
+
}
|
|
27
|
+
return derivation.from;
|
|
28
|
+
}
|
|
29
|
+
/** Targets ordered so every derived source is computed before its reader. */
|
|
30
|
+
export function orderFormDerivations(derive) {
|
|
31
|
+
const ordered = [];
|
|
32
|
+
const state = new Map();
|
|
33
|
+
const visit = (name, path) => {
|
|
34
|
+
const derivation = derive[name];
|
|
35
|
+
// skip: plain input field, nothing to order
|
|
36
|
+
if (derivation === undefined)
|
|
37
|
+
return;
|
|
38
|
+
const status = state.get(name);
|
|
39
|
+
// skip: already ordered via another path
|
|
40
|
+
if (status === "done")
|
|
41
|
+
return;
|
|
42
|
+
if (status === "visiting") {
|
|
43
|
+
throw new Error(`Form derivation cycle: ${[...path, name].join(" -> ")}`);
|
|
44
|
+
}
|
|
45
|
+
state.set(name, "visiting");
|
|
46
|
+
for (const dependency of dependenciesOf(derivation))
|
|
47
|
+
visit(dependency, [...path, name]);
|
|
48
|
+
state.set(name, "done");
|
|
49
|
+
ordered.push(name);
|
|
50
|
+
};
|
|
51
|
+
for (const name of Object.keys(derive))
|
|
52
|
+
visit(name, []);
|
|
53
|
+
return ordered;
|
|
54
|
+
}
|
|
55
|
+
function isNonEmptyList(value) {
|
|
56
|
+
return Array.isArray(value) && value.length > 0;
|
|
57
|
+
}
|
|
58
|
+
/** True when `field` is a `listSum` without `whenEmpty` whose list has no
|
|
59
|
+
* rows: the sum has nothing to add up, so the user types the value. */
|
|
60
|
+
export function isFormDerivationInputMode(screen, values, field) {
|
|
61
|
+
const derivation = screen.derive?.[field];
|
|
62
|
+
if (derivation === undefined || derivation.op !== "listSum")
|
|
63
|
+
return false;
|
|
64
|
+
if (derivation.whenEmpty !== undefined)
|
|
65
|
+
return false;
|
|
66
|
+
return !isPlainObject(values) || !isNonEmptyList(values[derivation.list]);
|
|
67
|
+
}
|
|
68
|
+
/** True when `field` is only read as a `whenEmpty` operand of `listSum`
|
|
69
|
+
* derivations whose lists all have rows: nothing reads it, so it is locked
|
|
70
|
+
* rather than inviting an input that is silently dropped. */
|
|
71
|
+
export function isUnusedFallbackOperand(screen, values, field) {
|
|
72
|
+
const derive = screen.derive;
|
|
73
|
+
if (derive === undefined || derive[field] !== undefined)
|
|
74
|
+
return false;
|
|
75
|
+
let usedAsFallback = false;
|
|
76
|
+
for (const derivation of Object.values(derive)) {
|
|
77
|
+
if (derivation.op !== "listSum") {
|
|
78
|
+
if (derivation.from.includes(field))
|
|
79
|
+
return false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (derivation.whenEmpty?.from.includes(field) !== true)
|
|
83
|
+
continue;
|
|
84
|
+
if (!isPlainObject(values) || !isNonEmptyList(values[derivation.list]))
|
|
85
|
+
return false;
|
|
86
|
+
usedAsFallback = true;
|
|
87
|
+
}
|
|
88
|
+
return usedAsFallback;
|
|
89
|
+
}
|
|
90
|
+
function sumListCell(rows, listField, cell) {
|
|
91
|
+
const embedded = listField?.type === "embedded" ? listField : undefined;
|
|
92
|
+
let sum = 0;
|
|
93
|
+
for (const rawRow of rows) {
|
|
94
|
+
// Recompute the row's derived cells so a manipulated row value can't leak into the head.
|
|
95
|
+
const row = embedded?.derived === undefined
|
|
96
|
+
? rawRow
|
|
97
|
+
: withDerivedCells(rawRow, embedded.derived, embedded.schema);
|
|
98
|
+
if (isPlainObject(row) && typeof row[cell] === "number")
|
|
99
|
+
sum += row[cell];
|
|
100
|
+
}
|
|
101
|
+
return sum;
|
|
102
|
+
}
|
|
103
|
+
function targetCurrency(field, current, sourceCurrency) {
|
|
104
|
+
if (isPlainObject(current) &&
|
|
105
|
+
typeof current["currency"] === "string" &&
|
|
106
|
+
current["currency"] !== "") {
|
|
107
|
+
return current["currency"];
|
|
108
|
+
}
|
|
109
|
+
if (sourceCurrency !== undefined)
|
|
110
|
+
return sourceCurrency;
|
|
111
|
+
if (field.type === "money" && field.currency.kind === "literal")
|
|
112
|
+
return field.currency.code;
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
/** Recomputes every derived head field of an action form from its inputs.
|
|
116
|
+
* Pure and client-safe: the renderer runs it live, the server runs it
|
|
117
|
+
* again (`withFormDerivations`) and overwrites whatever the client sent.
|
|
118
|
+
* Money operands are converted to minor units for the arithmetic and money
|
|
119
|
+
* targets are written back as `{ amount, currency }` in major units. */
|
|
120
|
+
export function applyFormDerivations(values, screen) {
|
|
121
|
+
if (!isPlainObject(values))
|
|
122
|
+
return values;
|
|
123
|
+
const derive = screen.derive;
|
|
124
|
+
if (derive === undefined)
|
|
125
|
+
return values;
|
|
126
|
+
const result = { ...values };
|
|
127
|
+
for (const target of orderFormDerivations(derive)) {
|
|
128
|
+
const derivation = derive[target];
|
|
129
|
+
const targetField = screen.fields[target];
|
|
130
|
+
if (derivation === undefined || targetField === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
let computed;
|
|
133
|
+
let sourceCurrency;
|
|
134
|
+
let ordinary;
|
|
135
|
+
if (derivation.op === "listSum") {
|
|
136
|
+
const rows = result[derivation.list];
|
|
137
|
+
if (isNonEmptyList(rows)) {
|
|
138
|
+
computed = sumListCell(rows, screen.fields[derivation.list], derivation.cell);
|
|
139
|
+
}
|
|
140
|
+
else if (derivation.whenEmpty === undefined) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
ordinary = derivation.whenEmpty;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
ordinary = derivation;
|
|
149
|
+
}
|
|
150
|
+
if (ordinary !== undefined) {
|
|
151
|
+
const operands = ordinary.from.map((name) => readOperand(result, screen.fields, name));
|
|
152
|
+
sourceCurrency = operands.find((operand) => operand.currency !== undefined)?.currency;
|
|
153
|
+
computed = computeDerivedCellValue(ordinary.op, operands.map((operand) => operand.value));
|
|
154
|
+
}
|
|
155
|
+
if (computed === undefined) {
|
|
156
|
+
delete result[target];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (targetField.type === "money") {
|
|
160
|
+
const minor = roundDerivedCellValue(computed, { type: "money" });
|
|
161
|
+
const currency = targetCurrency(targetField, result[target], sourceCurrency);
|
|
162
|
+
result[target] =
|
|
163
|
+
currency === undefined
|
|
164
|
+
? { amount: minor / MINOR_UNIT_SCALE }
|
|
165
|
+
: { amount: minor / MINOR_UNIT_SCALE, currency };
|
|
166
|
+
}
|
|
167
|
+
else if (targetField.type === "decimal") {
|
|
168
|
+
result[target] = roundDerivedCellValue(computed, targetField);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
result[target] = computed;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
|
|
|
45
45
|
export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.ts";
|
|
46
46
|
export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, type ManifestSystemEvent, serializeManifest, } from "./feature-manifest.ts";
|
|
47
47
|
export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.ts";
|
|
48
|
+
export type { FormDerivationScreen } from "./form-derivations.ts";
|
|
49
|
+
export { applyFormDerivations, isFormDerivationInputMode } from "./form-derivations.ts";
|
|
48
50
|
export { HANDLER_FIELD_REFERENCES_META_KEY, type HandlerFieldPathSegment, type HandlerFieldReference, type HandlerFieldReferenceAtPath, type HandlerFieldReferenceMeta, handlerFieldReference, handlerFieldReferences, readHandlerFieldReferences, } from "./handler-field-references.ts";
|
|
49
51
|
export { resolveName, withResponseData } from "./handler-helpers.ts";
|
|
50
52
|
export { i18nKey } from "./i18n-key.ts";
|
|
@@ -85,7 +87,8 @@ export { assertNotificationVariables, templateVariable, templateVariablesToZod,
|
|
|
85
87
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.ts";
|
|
86
88
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.ts";
|
|
87
89
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.ts";
|
|
88
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, ProviderPluginContext, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
|
|
90
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, ProviderPluginContext, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateImagesVariableDef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
|
|
89
91
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.ts";
|
|
90
92
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.ts";
|
|
91
93
|
export { runValidation } from "./validation.ts";
|
|
94
|
+
export { formDerivationsOfSchema, withFormDerivations } from "./with-form-derivations.ts";
|
package/dist/engine/index.js
CHANGED
|
@@ -40,6 +40,7 @@ export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, group
|
|
|
40
40
|
export { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
|
|
41
41
|
export { buildManifestFromRegistry, serializeManifest, } from "./feature-manifest.js";
|
|
42
42
|
export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
|
|
43
|
+
export { applyFormDerivations, isFormDerivationInputMode } from "./form-derivations.js";
|
|
43
44
|
export { HANDLER_FIELD_REFERENCES_META_KEY, handlerFieldReference, handlerFieldReferences, readHandlerFieldReferences, } from "./handler-field-references.js";
|
|
44
45
|
export { resolveName, withResponseData } from "./handler-helpers.js";
|
|
45
46
|
export { i18nKey } from "./i18n-key.js";
|
|
@@ -80,3 +81,4 @@ export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTra
|
|
|
80
81
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
81
82
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
82
83
|
export { runValidation } from "./validation.js";
|
|
84
|
+
export { formDerivationsOfSchema, withFormDerivations } from "./with-form-derivations.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// 2 decimal places (cents) — covers every currently-supported currency
|
|
2
|
+
// (EUR/USD/GBP/...). No ISO-4217 minor-unit table yet (JPY=0, BHD=3) —
|
|
3
|
+
// upgrade path once a currency needing a different scale actually lands.
|
|
4
|
+
// Client-safe on purpose: db/money.ts pulls in engine runtime values.
|
|
5
|
+
export const MINOR_UNIT_SCALE = 100;
|
|
6
|
+
export function toMinorUnits(amount) {
|
|
7
|
+
return Math.round(amount * MINOR_UNIT_SCALE);
|
|
8
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyEntityEvent } from "../db/apply-entity-event.js";
|
|
2
2
|
import { assertBackingTableSuperset, deriveEntityTableMeta } from "../db/entity-table-meta.js";
|
|
3
|
+
import { markExecutorManagedTable } from "../db/executor-managed-tables.js";
|
|
3
4
|
import { asEntityTableMeta } from "../db/query.js";
|
|
4
5
|
import { buildEntityTable } from "../db/table-builder.js";
|
|
5
6
|
import { qualifyEntityName } from "./qualified-name.js";
|
|
@@ -20,6 +21,7 @@ export function buildImplicitProjection(featureName, entityName, entity, qualify
|
|
|
20
21
|
const drizzleTable = backingTable !== undefined
|
|
21
22
|
? resolveBackingTable(entityName, entity, backingTable)
|
|
22
23
|
: buildEntityTable(entityName, entity);
|
|
24
|
+
markExecutorManagedTable(drizzleTable);
|
|
23
25
|
// applyEntityEvent gibt ApplyResult zurück; SingleStreamApplyFn erwartet
|
|
24
26
|
// Promise<void>. Im rebuild-Pfad ist die Row irrelevant — wir discarden.
|
|
25
27
|
const handler = async (event, tx) => {
|
|
@@ -476,8 +476,11 @@ export function validateProjectionApplyKeys(state) {
|
|
|
476
476
|
// or `jobRun` (BullMQ-callback-driven lifecycle, no executor).
|
|
477
477
|
// A "Shape-Anchor"-entity is no longer needed for this case.
|
|
478
478
|
const isEventsOnlySource = !sources.every((src) => state.entityMap.has(src));
|
|
479
|
+
// An extension source may be a stream whose events the executor writes under a
|
|
480
|
+
// name that is no registered entity (document-extract); its auto-verbs count.
|
|
481
|
+
const extensionSources = projDef.extraSources ?? [];
|
|
479
482
|
for (const src of rebuildSources) {
|
|
480
|
-
if (state.entityMap.has(src)) {
|
|
483
|
+
if (state.entityMap.has(src) || extensionSources.includes(src)) {
|
|
481
484
|
for (const verb of ENTITY_LIFECYCLE_VERBS)
|
|
482
485
|
validEventTypes.add(`${src}.${verb}`);
|
|
483
486
|
}
|
|
@@ -236,12 +236,26 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
236
236
|
// whether the key may be omitted at all is decided by buildInsertSchema
|
|
237
237
|
// off the same flag. `minItems` overrides that default when set.
|
|
238
238
|
let list = z.array(row);
|
|
239
|
-
const min = field.minItems ?? (field.required === true ? 1 : undefined);
|
|
240
|
-
if (min !== undefined)
|
|
241
|
-
list = list.min(min);
|
|
242
239
|
if (field.maxItems !== undefined)
|
|
243
240
|
list = list.max(field.maxItems);
|
|
244
|
-
|
|
241
|
+
const min = field.minItems ?? (field.required === true ? 1 : undefined);
|
|
242
|
+
if (min === undefined)
|
|
243
|
+
return list;
|
|
244
|
+
// A custom issue rather than min(): other arrays (multiSelect) share zod's
|
|
245
|
+
// too_small and must not read "rows"; the bridge honours params.i18nKey.
|
|
246
|
+
return list.superRefine((rows, ctx) => {
|
|
247
|
+
// skip: the minimum is met, nothing to report
|
|
248
|
+
if (rows.length >= min)
|
|
249
|
+
return;
|
|
250
|
+
ctx.addIssue({
|
|
251
|
+
code: "custom",
|
|
252
|
+
input: rows,
|
|
253
|
+
params: {
|
|
254
|
+
i18nKey: `errors.validation.list_min_rows.${min === 1 ? "one" : "other"}`,
|
|
255
|
+
minimum: min,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
245
259
|
}
|
|
246
260
|
case "jsonb": {
|
|
247
261
|
// Free-form jsonb — keys sind tenant-/runtime-defined. Validation
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FieldDefinition, SelectFieldDef } from "./types/fields.ts";
|
|
2
2
|
import type { ActionFormScreenDefinition, ConfigEditScreenDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, FieldCondition, FormatSpec, ListColumnSpec, ProjectionDetailScreenDefinition, ScreenDefinition, SecretMintScreenDefinition } from "./types/screen.ts";
|
|
3
|
+
export declare const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
|
|
3
4
|
export declare function explicitListScreenId(screen: ScreenDefinition): string | undefined;
|
|
4
5
|
export declare function resolveNavParentScreen(screens: readonly ScreenDefinition[], detail: ScreenDefinition, getId: (screen: ScreenDefinition) => string): ScreenDefinition | undefined;
|
|
5
6
|
export type EditLayoutScreen = ProjectionDetailScreenDefinition | EntityEditScreenDefinition | ActionFormScreenDefinition | ConfigEditScreenDefinition | SecretMintScreenDefinition;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Entity name the renderer gives the synthesized entity of an actionForm, secretMint or confirm
|
|
2
|
+
// step. It is not registered, so nothing server-side may treat it as a real entity.
|
|
3
|
+
export const ACTION_FORM_PSEUDO_ENTITY = "__action-form__";
|
|
1
4
|
// Every screen type carrying `listScreenId` declares it identically; centralizing
|
|
2
5
|
// here keeps the switch the only place that grows for a new screen type.
|
|
3
6
|
export function explicitListScreenId(screen) {
|