@cosmicdrift/kumiko-framework 0.353.0 → 0.354.1
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/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +16 -10
- package/dist/api/request-context.d.ts +1 -0
- package/dist/api/request-context.js +5 -0
- package/dist/api/routes.js +4 -2
- package/dist/api/server.js +28 -3
- package/dist/api/sse-broker.d.ts +7 -3
- package/dist/api/sse-route.js +2 -6
- package/dist/api/token-expiry-timer.d.ts +1 -0
- package/dist/api/token-expiry-timer.js +10 -0
- package/dist/bun-db/query.js +29 -2
- package/dist/crypto/event-pii.js +4 -2
- package/dist/crypto/kms-wiring.js +31 -5
- package/dist/db/eagerload.d.ts +8 -3
- package/dist/db/eagerload.js +85 -81
- package/dist/db/event-store-executor-context.d.ts +12 -0
- package/dist/db/event-store-executor-context.js +59 -18
- package/dist/db/event-store-executor-read.js +81 -31
- package/dist/db/event-store-executor-write.js +54 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/queries/backfill-pii.d.ts +1 -0
- package/dist/db/queries/backfill-pii.js +11 -6
- package/dist/db/queries/event-consumer.d.ts +2 -3
- package/dist/db/queries/event-consumer.js +54 -43
- package/dist/db/queries/event-store.d.ts +5 -0
- package/dist/db/queries/event-store.js +51 -3
- package/dist/db/queries/pii-backfill-state.d.ts +8 -1
- package/dist/db/queries/pii-backfill-state.js +16 -5
- package/dist/db/queries/shadow-swap.js +32 -3
- package/dist/db/render-ddl.js +6 -6
- package/dist/db/table-builder.js +6 -7
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +47 -7
- package/dist/derivatives/variant-key.d.ts +1 -0
- package/dist/derivatives/variant-key.js +3 -0
- package/dist/engine/boot-validator/access-declarations.js +5 -0
- package/dist/engine/boot-validator/config-deps.js +3 -0
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +40 -0
- package/dist/engine/boot-validator/handler-field-references.d.ts +2 -0
- package/dist/engine/boot-validator/handler-field-references.js +25 -0
- package/dist/engine/boot-validator/index.js +6 -1
- package/dist/engine/boot-validator/nav.js +3 -1
- package/dist/engine/boot-validator/ownership.d.ts +2 -1
- package/dist/engine/boot-validator/ownership.js +32 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -1
- package/dist/engine/boot-validator/query-output-columns.js +9 -9
- package/dist/engine/boot-validator/screens.js +98 -7
- package/dist/engine/boot-validator/transfer-graph.d.ts +2 -1
- package/dist/engine/boot-validator/transfer-graph.js +8 -63
- package/dist/engine/boot-validator/url-prefill-fields.d.ts +1 -0
- package/dist/engine/boot-validator/url-prefill-fields.js +35 -0
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/engine/changeset-changes.d.ts +1 -0
- package/dist/engine/changeset-changes.js +49 -33
- package/dist/engine/define-feature.js +11 -0
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/entity-handlers.js +55 -34
- package/dist/engine/escape-hatch-reason.d.ts +2 -0
- package/dist/engine/escape-hatch-reason.js +8 -0
- package/dist/engine/extension-names.d.ts +8 -9
- package/dist/engine/extension-names.js +8 -9
- package/dist/engine/extensions/tenant-data.d.ts +3 -1
- package/dist/engine/extensions/tenant-resource.d.ts +9 -1
- package/dist/engine/factories.d.ts +1 -1
- package/dist/engine/feature-changelog.d.ts +2 -0
- package/dist/engine/feature-changelog.js +6 -0
- package/dist/engine/feature-config-events-jobs.js +41 -4
- package/dist/engine/handler-field-references.d.ts +3 -0
- package/dist/engine/handler-field-references.js +31 -0
- package/dist/engine/i18n-key.js +2 -3
- package/dist/engine/index.d.ts +8 -2
- package/dist/engine/index.js +6 -1
- package/dist/engine/ownership.js +3 -4
- package/dist/engine/project-app-schema-for-roles.js +3 -1
- package/dist/engine/registry-ingest.d.ts +1 -0
- package/dist/engine/registry-ingest.js +8 -3
- package/dist/engine/run-pipeline.js +12 -1
- package/dist/engine/schema-builder.js +35 -2
- package/dist/engine/screen-helpers.d.ts +13 -0
- package/dist/engine/screen-helpers.js +31 -0
- package/dist/engine/soft-delete-cleanup.js +3 -2
- package/dist/engine/steps/_read-source.d.ts +4 -0
- package/dist/engine/steps/_read-source.js +22 -0
- package/dist/engine/steps/describe-workflow-step-error.js +3 -1
- package/dist/engine/steps/read-find-many.js +2 -1
- package/dist/engine/steps/read-find-one.js +2 -1
- package/dist/engine/steps/workflow-step-error.d.ts +5 -0
- package/dist/engine/steps/workflow-step-error.js +13 -0
- package/dist/engine/transfer-adjacency.d.ts +24 -0
- package/dist/engine/transfer-adjacency.js +107 -0
- package/dist/entrypoint/index.d.ts +2 -0
- package/dist/entrypoint/index.js +2 -0
- package/dist/env/index.js +45 -23
- package/dist/errors/classes.d.ts +8 -0
- package/dist/errors/classes.js +13 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +2 -1
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/errors/subject-erased.d.ts +2 -0
- package/dist/errors/subject-erased.js +9 -0
- package/dist/event-store/admin-api.js +14 -10
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +14 -9
- package/dist/event-store/events-schema.js +2 -1
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +30 -6
- package/dist/files/file-routes.js +10 -4
- package/dist/files/resolve-content-type.d.ts +2 -0
- package/dist/files/resolve-content-type.js +8 -0
- package/dist/i18n/required-surface-keys.d.ts +2 -0
- package/dist/i18n/required-surface-keys.js +8 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +13 -0
- package/dist/jobs/job-runner.js +128 -22
- package/dist/migrations/pii-event-backfill.js +46 -25
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/standard-metrics.d.ts +4 -0
- package/dist/observability/standard-metrics.js +12 -0
- package/dist/pipeline/dispatch-shared.d.ts +2 -0
- package/dist/pipeline/dispatch-shared.js +74 -9
- package/dist/pipeline/dispatch-stream.d.ts +5 -1
- package/dist/pipeline/dispatch-stream.js +19 -12
- package/dist/pipeline/dispatcher.d.ts +3 -1
- package/dist/pipeline/dispatcher.js +10 -15
- package/dist/pipeline/event-consumer-state.js +0 -26
- package/dist/pipeline/event-dispatcher-admin.js +17 -11
- package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -1
- package/dist/pipeline/event-dispatcher-delivery.js +3 -2
- package/dist/pipeline/event-dispatcher.js +48 -7
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/member-read-only-transaction.js +25 -5
- package/dist/pipeline/msp-rebuild.js +3 -3
- package/dist/pipeline/pending-gap-ranges.d.ts +1 -0
- package/dist/pipeline/pending-gap-ranges.js +22 -0
- package/dist/pipeline/system-hooks.js +4 -4
- package/dist/pipeline/system-identity-switch.d.ts +1 -1
- package/dist/pipeline/system-identity-switch.js +5 -4
- package/dist/pipeline/tenant-timezone-cache.js +5 -3
- package/dist/redis/cache-sync-topics.d.ts +1 -0
- package/dist/redis/cache-sync-topics.js +1 -0
- package/dist/redis/client.d.ts +1 -0
- package/dist/redis/client.js +7 -0
- package/dist/redis/index.d.ts +1 -1
- package/dist/redis/index.js +1 -1
- package/dist/redis/pubsub-signal.d.ts +1 -0
- package/dist/redis/pubsub-signal.js +3 -0
- package/dist/search/meilisearch-adapter.js +34 -4
- package/dist/stack/test-stack.d.ts +4 -1
- package/dist/stack/test-stack.js +9 -2
- package/dist/testing/e2e-generator.js +7 -2
- package/dist/ui-types/index.d.ts +3 -3
- package/dist/ui-types/index.js +2 -2
- package/dist/upgrade-cli.js +7 -2
- package/package.json +12 -4
- package/src/changes.json +209 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +2 -1
|
@@ -12,7 +12,7 @@ import { isEncryptedAtRest } from "../config-helpers.js";
|
|
|
12
12
|
import { parseRefTarget } from "../parse-ref-target.js";
|
|
13
13
|
import { isKebabSegment, isValidQn, qualifyEntityName } from "../qualified-name.js";
|
|
14
14
|
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js";
|
|
15
|
-
import { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
15
|
+
import { explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../screen-helpers.js";
|
|
16
16
|
import { metricField } from "../types/index.js";
|
|
17
17
|
import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
|
|
18
18
|
import { isSelectOptionTone } from "./select-option-tone.js";
|
|
@@ -127,6 +127,11 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
|
|
|
127
127
|
`the params extractor or retarget to an actionForm / secretMint / cross-entity ` +
|
|
128
128
|
`entityEdit-create screen.`);
|
|
129
129
|
}
|
|
130
|
+
const formTarget = target.screen;
|
|
131
|
+
if ((formTarget.type === "actionForm" || formTarget.type === "secretMint") &&
|
|
132
|
+
formTarget.submitPrefilled === true) {
|
|
133
|
+
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) rowAction "${action.id}"`, "navigate-target", action.screen ?? `entity "${action.entity}"`, formTarget, action.params);
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
136
|
// Shared by projectionDetail and entityEdit: a tab strip needs something to
|
|
132
137
|
// label each tab with and a stable id per tab — the id anchors the ?tab=
|
|
@@ -353,9 +358,9 @@ function validateDrawerTargetAction(featureName, screenId, screenKind, actionLab
|
|
|
353
358
|
// skip: no params extractor — nothing to check against the target's fields.
|
|
354
359
|
if (action.params === undefined)
|
|
355
360
|
return;
|
|
356
|
-
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) ${actionLabel} "${action.id}"`, action.screen, target, action.params);
|
|
361
|
+
validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) ${actionLabel} "${action.id}"`, "drawer-target", action.screen, target, action.params);
|
|
357
362
|
}
|
|
358
|
-
function validateDrawerPrefillKeys(where, targetScreenId, target, params) {
|
|
363
|
+
function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
|
|
359
364
|
const renderedFieldNames = new Set();
|
|
360
365
|
for (const section of target.layout.sections) {
|
|
361
366
|
if (!isFieldsEditSection(section))
|
|
@@ -365,7 +370,7 @@ function validateDrawerPrefillKeys(where, targetScreenId, target, params) {
|
|
|
365
370
|
}
|
|
366
371
|
for (const fieldName of rowFieldExtractorKeys(params)) {
|
|
367
372
|
if (!Object.hasOwn(target.fields, fieldName)) {
|
|
368
|
-
throw new Error(`${where} params prefills "${fieldName}", which
|
|
373
|
+
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" does not declare as a ` +
|
|
369
374
|
`field — the renderer would drop it and leave the form empty. Target fields: ` +
|
|
370
375
|
`${Object.keys(target.fields).sort().join(", ") || "(none)"}.`);
|
|
371
376
|
}
|
|
@@ -374,11 +379,11 @@ function validateDrawerPrefillKeys(where, targetScreenId, target, params) {
|
|
|
374
379
|
(("sensitive" in targetField && targetField.sensitive === true) ||
|
|
375
380
|
("format" in targetField && targetField.format === "password"));
|
|
376
381
|
if (isSensitiveOrPassword) {
|
|
377
|
-
throw new Error(`${where} params prefills "${fieldName}", which
|
|
382
|
+
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" marks as ` +
|
|
378
383
|
`sensitive or password — the renderer never prefills those fields and would leave the form empty.`);
|
|
379
384
|
}
|
|
380
385
|
if (!renderedFieldNames.has(fieldName)) {
|
|
381
|
-
throw new Error(`${where} params prefills "${fieldName}", which
|
|
386
|
+
throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" does not render in ` +
|
|
382
387
|
`its layout — the renderer only prefills fields the layout shows and would drop it.`);
|
|
383
388
|
}
|
|
384
389
|
}
|
|
@@ -519,9 +524,13 @@ function rejectWriteOnlyFormField(featureName, screenId, context, fieldName, fde
|
|
|
519
524
|
// payload refs must name another field of the same form (the payload also feeds
|
|
520
525
|
// optionsAvailabilityQuery). The QN existence check
|
|
521
526
|
// lives in query-refs.ts with the other query refs.
|
|
527
|
+
const CONDITIONAL_OPTIONS_ENTITY_ONLY = "conditionalOptions is only supported on entity select fields (enforced by the entity write path)";
|
|
522
528
|
function validateFormSelectOptions(featureName, screenId, context, fieldName, fdef, siblingFieldNames) {
|
|
523
529
|
if (fdef.type === "select") {
|
|
524
530
|
const where = `[Feature ${featureName}] Screen "${screenId}" (${context}) select field "${fieldName}"`;
|
|
531
|
+
if (fdef.conditionalOptions !== undefined) {
|
|
532
|
+
throw new Error(`${where} declares conditionalOptions — ${CONDITIONAL_OPTIONS_ENTITY_ONLY}`);
|
|
533
|
+
}
|
|
525
534
|
if (fdef.optionsQuery !== undefined) {
|
|
526
535
|
if (fdef.optionsQuery.length === 0) {
|
|
527
536
|
throw new Error(`${where} has an empty optionsQuery`);
|
|
@@ -649,6 +658,17 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, allScre
|
|
|
649
658
|
`has leading/trailing whitespace — it would never match a payload field.`);
|
|
650
659
|
}
|
|
651
660
|
validateScreenNavTarget(feature.name, screenId, screenKind, "redirect", redirectTarget, allScreenQns, feature.screens);
|
|
661
|
+
// idFrom only means something to a target that carries a record id; the
|
|
662
|
+
// renderer drops it silently otherwise. Cross-feature QN targets can't be
|
|
663
|
+
// typed from here and stay unchecked.
|
|
664
|
+
const sameFeatureTarget = feature.screens[redirectTarget];
|
|
665
|
+
if (typeof redirect !== "string" &&
|
|
666
|
+
sameFeatureTarget !== undefined &&
|
|
667
|
+
sameFeatureTarget.type !== "entityEdit" &&
|
|
668
|
+
sameFeatureTarget.type !== "projectionDetail") {
|
|
669
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screenKind}) redirect.idFrom is set but target screen ` +
|
|
670
|
+
`"${redirectTarget}" (${sameFeatureTarget.type}) carries no id — use the plain string form.`);
|
|
671
|
+
}
|
|
652
672
|
}
|
|
653
673
|
// redirect is only honored on record actions; on list/related-list row
|
|
654
674
|
// actions it would be silently dropped, so the author would believe it works.
|
|
@@ -781,6 +801,20 @@ function getNavAreaIndex(featureMap) {
|
|
|
781
801
|
navAreaIndexByFeatureMap.set(featureMap, index);
|
|
782
802
|
return index;
|
|
783
803
|
}
|
|
804
|
+
// resolveNavParentScreen falls back to a heuristic when the explicit
|
|
805
|
+
// listScreenId matches no screen, so a typo would silently get the guess.
|
|
806
|
+
function validateExplicitListScreenExists(featureName, screenId, screen, screensByShortId) {
|
|
807
|
+
const listScreenId = explicitListScreenId(screen);
|
|
808
|
+
// skip: no explicit listScreenId, the nav heuristic applies and there is nothing to validate
|
|
809
|
+
if (listScreenId === undefined)
|
|
810
|
+
return;
|
|
811
|
+
const shortId = listScreenId.slice(listScreenId.lastIndexOf(":") + 1);
|
|
812
|
+
// skip: the explicit listScreenId resolves to a mounted screen
|
|
813
|
+
if (screensByShortId.has(shortId))
|
|
814
|
+
return;
|
|
815
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) listScreenId "${listScreenId}" ` +
|
|
816
|
+
`matches no mounted screen — fix the id or mount the list screen.`);
|
|
817
|
+
}
|
|
784
818
|
// Every screen must resolve nav via `nav`, `r.nav()`, a parent list, or
|
|
785
819
|
// `dormant: true`. Skipped when the composed set has no nav entries at all.
|
|
786
820
|
function validateScreenHasNavArea(feature, screenId, screen, featureMap) {
|
|
@@ -925,6 +959,22 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
925
959
|
detailForScreens,
|
|
926
960
|
};
|
|
927
961
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
962
|
+
validateExplicitListScreenExists(feature.name, screenId, screen, screensByShortId);
|
|
963
|
+
// Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
|
|
964
|
+
if ((screen.type === "entityEdit" ||
|
|
965
|
+
screen.type === "actionForm" ||
|
|
966
|
+
screen.type === "secretMint") &&
|
|
967
|
+
screen.urlPrefillFields !== undefined) {
|
|
968
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets urlPrefillFields — ` +
|
|
969
|
+
`it is derived from navigate params by buildAppSchema; declare params on the navigating action instead.`);
|
|
970
|
+
}
|
|
971
|
+
if (screen.type !== "actionForm" &&
|
|
972
|
+
screen.type !== "secretMint" &&
|
|
973
|
+
"submitPrefilled" in screen &&
|
|
974
|
+
screen.submitPrefilled !== undefined) {
|
|
975
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled — ` +
|
|
976
|
+
`only actionForm and secretMint screens support it. Remove it.`);
|
|
977
|
+
}
|
|
928
978
|
validateScreenHasNavArea(feature, screenId, screen, featureMap);
|
|
929
979
|
validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, featureMap);
|
|
930
980
|
if (screen.type === "custom") {
|
|
@@ -948,6 +998,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
948
998
|
for (const col of screen.columns) {
|
|
949
999
|
const normalizedCol = normalizeListColumn(col);
|
|
950
1000
|
validateColumnRendererForm(feature.name, screenId, normalizedCol);
|
|
1001
|
+
rejectInertColumnSortable(feature.name, screenId, "projectionList", normalizedCol);
|
|
951
1002
|
if (normalizedCol.refEntity !== undefined) {
|
|
952
1003
|
assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, feature.name, featureMap);
|
|
953
1004
|
}
|
|
@@ -1411,6 +1462,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1411
1462
|
throw new Error(buildUnknownFieldMessage(feature.name, screenId, normalized.field, screen.entity, columnFieldNames));
|
|
1412
1463
|
}
|
|
1413
1464
|
validateColumnRendererForm(feature.name, screenId, normalized);
|
|
1465
|
+
rejectInertColumnSortable(feature.name, screenId, "entityList", normalized);
|
|
1414
1466
|
if (normalized.refEntity !== undefined) {
|
|
1415
1467
|
assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (entityList)`, `column "${normalized.field}" (refEntity)`, normalized.refEntity, feature.name, featureMap);
|
|
1416
1468
|
}
|
|
@@ -1811,6 +1863,12 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
|
1811
1863
|
throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
1812
1864
|
}
|
|
1813
1865
|
validateStackedAreaOnlyProps(where, panel);
|
|
1866
|
+
validateStackedAreaEnumProp(where, "dateFormat", panel.dateFormat, ["day", "month"]);
|
|
1867
|
+
validateStackedAreaEnumProp(where, "initialWindow", panel.initialWindow, [
|
|
1868
|
+
"default-range",
|
|
1869
|
+
"from-today",
|
|
1870
|
+
]);
|
|
1871
|
+
validateStackedAreaEnumProp(where, "markerLegend", panel.markerLegend, ["list", "legend"]);
|
|
1814
1872
|
const nonBooleanProp = ["brush", "legendTotals"].find((prop) => panel[prop] !== undefined && typeof panel[prop] !== "boolean");
|
|
1815
1873
|
if (nonBooleanProp !== undefined) {
|
|
1816
1874
|
throw new Error(`${where} has a non-boolean ${nonBooleanProp}.`);
|
|
@@ -1826,11 +1884,27 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
|
1826
1884
|
if (panel.ranges !== undefined)
|
|
1827
1885
|
validateDashboardChartRanges(`${where} ranges`, panel.ranges);
|
|
1828
1886
|
}
|
|
1887
|
+
function validateStackedAreaEnumProp(where, prop, value, allowed) {
|
|
1888
|
+
// skip: optional prop, nothing to validate when unset
|
|
1889
|
+
if (value === undefined)
|
|
1890
|
+
return;
|
|
1891
|
+
if (!allowed.includes(value)) {
|
|
1892
|
+
throw new Error(`${where}.${prop} "${String(value)}" must be one of ${allowed.join(", ")}.`);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1829
1895
|
function validateStackedAreaOnlyProps(where, panel) {
|
|
1830
1896
|
// skip: stacked-area is the one chart kind that supports these props
|
|
1831
1897
|
if (panel.chart === "stacked-area")
|
|
1832
1898
|
return;
|
|
1833
|
-
const stackedAreaOnly = [
|
|
1899
|
+
const stackedAreaOnly = [
|
|
1900
|
+
"markerKinds",
|
|
1901
|
+
"legendTotals",
|
|
1902
|
+
"ranges",
|
|
1903
|
+
"brush",
|
|
1904
|
+
"dateFormat",
|
|
1905
|
+
"initialWindow",
|
|
1906
|
+
"markerLegend",
|
|
1907
|
+
].filter((prop) => panel[prop] !== undefined);
|
|
1834
1908
|
if (stackedAreaOnly.length > 0) {
|
|
1835
1909
|
throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
1836
1910
|
}
|
|
@@ -1851,6 +1925,9 @@ function validateDashboardChartMarkerKind(where, look) {
|
|
|
1851
1925
|
if (look.color !== undefined && (typeof look.color !== "string" || look.color.trim() === "")) {
|
|
1852
1926
|
throw new Error(`${where}.color must be a non-empty CSS color.`);
|
|
1853
1927
|
}
|
|
1928
|
+
if (look.label !== undefined && (typeof look.label !== "string" || look.label.trim() === "")) {
|
|
1929
|
+
throw new Error(`${where}.label must be a non-empty i18n key.`);
|
|
1930
|
+
}
|
|
1854
1931
|
}
|
|
1855
1932
|
function validateDashboardChartRanges(where, ranges) {
|
|
1856
1933
|
if (ranges.options.length === 0) {
|
|
@@ -1949,6 +2026,20 @@ function validateDashboardFilterDefinition(featureName, screenId, filter) {
|
|
|
1949
2026
|
// stimmt: wenn `react` als Object gesetzt ist, MUSS `__component` ein
|
|
1950
2027
|
// nicht-leerer String sein. Ein client-seitig ausgelassener Key löst
|
|
1951
2028
|
// nur eine Warnung aus, kein Boot-Fail.
|
|
2029
|
+
// Column-level sortable: true only enables relatedList headers. On entityList
|
|
2030
|
+
// the entity field's flag wins; on projectionList sortable comes from the
|
|
2031
|
+
// query schema and a column can only opt OUT with sortable: false.
|
|
2032
|
+
function rejectInertColumnSortable(featureName, screenId, screenType, column) {
|
|
2033
|
+
const isInert = screenType === "entityList" ? column.sortable !== undefined : column.sortable === true;
|
|
2034
|
+
// skip: the sortable flag is meaningful for this screen type
|
|
2035
|
+
if (!isInert)
|
|
2036
|
+
return;
|
|
2037
|
+
const source = screenType === "entityList"
|
|
2038
|
+
? "set sortable: true on the entity field instead"
|
|
2039
|
+
: "sorting is derived from the query's Zod schema; only sortable: false is honored";
|
|
2040
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) column "${column.field}" sets sortable — ` +
|
|
2041
|
+
`it is ignored here; ${source}.`);
|
|
2042
|
+
}
|
|
1952
2043
|
export function validateColumnRendererForm(featureName, screenId, column) {
|
|
1953
2044
|
const renderer = column.renderer;
|
|
1954
2045
|
// skip: nur die PlatformComponent-Form ({ react: { __component: "..." } })
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_TRANSFER_DEPTH } from "../transfer-adjacency.js";
|
|
1
2
|
import type { FeatureDefinition } from "../types/index.js";
|
|
2
|
-
export
|
|
3
|
+
export { MAX_TRANSFER_DEPTH };
|
|
3
4
|
export declare function validateTransferGraph(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseRefTargetEntityName } from "../parse-ref-target.js";
|
|
2
|
+
import { findOverDeepTransferChain, MAX_TRANSFER_DEPTH, resolveTransferAdjacency, } from "../transfer-adjacency.js";
|
|
2
3
|
// --- Transfer-graph boot validation (fw#3088) ---
|
|
3
4
|
//
|
|
4
5
|
// tenant-handover walks an entity graph across the tenant boundary along two
|
|
@@ -9,16 +10,7 @@ import { parseRefTargetEntityName } from "../parse-ref-target.js";
|
|
|
9
10
|
//
|
|
10
11
|
// Scoped to entities declaring `transferable: true`, so a consumer that has
|
|
11
12
|
// one of these shapes but never hands that entity over is unaffected.
|
|
12
|
-
|
|
13
|
-
// resolver in bundled-features because the dependency only runs that way:
|
|
14
|
-
// bundled-features imports from the framework, never the reverse.
|
|
15
|
-
//
|
|
16
|
-
// Guards against a schema whose reference edges span more levels than anyone
|
|
17
|
-
// intended — a runaway graph would move rows an operator never associated with
|
|
18
|
-
// the handover. Deliberately a constant and not per-feature config: the limit
|
|
19
|
-
// is a safety net, and a configurable one gets raised by whoever trips it
|
|
20
|
-
// instead of prompting them to reconsider their graph.
|
|
21
|
-
export const MAX_TRANSFER_DEPTH = 5;
|
|
13
|
+
export { MAX_TRANSFER_DEPTH };
|
|
22
14
|
function allEntities(featureMap) {
|
|
23
15
|
const out = [];
|
|
24
16
|
for (const feature of featureMap.values()) {
|
|
@@ -28,17 +20,6 @@ function allEntities(featureMap) {
|
|
|
28
20
|
}
|
|
29
21
|
return out;
|
|
30
22
|
}
|
|
31
|
-
function referenceTargets(entity) {
|
|
32
|
-
const targets = [];
|
|
33
|
-
for (const field of Object.values(entity.fields)) {
|
|
34
|
-
if (field.type !== "reference")
|
|
35
|
-
continue;
|
|
36
|
-
if (field.multiple === true)
|
|
37
|
-
continue;
|
|
38
|
-
targets.push(parseRefTargetEntityName(field.entity));
|
|
39
|
-
}
|
|
40
|
-
return targets;
|
|
41
|
-
}
|
|
42
23
|
// A `multiple` reference stores a jsonb array of ids, which the handover's
|
|
43
24
|
// `= ANY($ids)` column match cannot address. Declaring one towards a transferable
|
|
44
25
|
// entity would mean its rows never move with their host. A target outside the
|
|
@@ -52,6 +33,8 @@ function validateNoMultipleReferenceEdge(entry, featureName, transferable) {
|
|
|
52
33
|
for (const [fieldName, field] of Object.entries(entry.entity.fields)) {
|
|
53
34
|
if (field.type !== "reference" || field.multiple !== true)
|
|
54
35
|
continue;
|
|
36
|
+
if (field.handover === "stay")
|
|
37
|
+
continue;
|
|
55
38
|
if (!transferable.has(parseRefTargetEntityName(field.entity)))
|
|
56
39
|
continue;
|
|
57
40
|
throw new Error(`[Kumiko TransferGraph] entity "${entry.name}" declares transferable: true and a ` +
|
|
@@ -62,31 +45,6 @@ function validateNoMultipleReferenceEdge(entry, featureName, transferable) {
|
|
|
62
45
|
`from "${entry.name}".`);
|
|
63
46
|
}
|
|
64
47
|
}
|
|
65
|
-
// Depth is measured over reference edges between transferable entities, the
|
|
66
|
-
// only chain that nests (parentRef is one level by construction — see
|
|
67
|
-
// engine/boot-validator/parent-ref.ts).
|
|
68
|
-
//
|
|
69
|
-
// `onPath` counts NODES, the limit counts EDGES: a chain of exactly
|
|
70
|
-
// MAX_TRANSFER_DEPTH edges holds MAX_TRANSFER_DEPTH + 1 entities, and the
|
|
71
|
-
// mover runs MAX_TRANSFER_DEPTH rounds of one hop each, so it still walks that
|
|
72
|
-
// chain whole. Rejecting it here would make a schema the mover handles
|
|
73
|
-
// correctly refuse to boot.
|
|
74
|
-
function longestTransferableChain(startName, bySource, onPath) {
|
|
75
|
-
if (onPath.length > MAX_TRANSFER_DEPTH + 1)
|
|
76
|
-
return onPath;
|
|
77
|
-
for (const target of bySource.get(startName) ?? []) {
|
|
78
|
-
// skip: a cycle revisits a type already on this path, and schema depth
|
|
79
|
-
// cannot measure how far it actually runs — that depends on the rows, not
|
|
80
|
-
// the declaration. The mover carries this one instead, failing with
|
|
81
|
-
// `transfer_graph_too_deep` when its rounds run out (#3131).
|
|
82
|
-
if (onPath.includes(target))
|
|
83
|
-
continue;
|
|
84
|
-
const deeper = longestTransferableChain(target, bySource, [...onPath, target]);
|
|
85
|
-
if (deeper !== undefined)
|
|
86
|
-
return deeper;
|
|
87
|
-
}
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
48
|
export function validateTransferGraph(feature, featureMap) {
|
|
91
49
|
const transferable = new Set(allEntities(featureMap)
|
|
92
50
|
.filter((e) => e.entity.transferable === true)
|
|
@@ -94,30 +52,17 @@ export function validateTransferGraph(feature, featureMap) {
|
|
|
94
52
|
for (const [name, entity] of Object.entries(feature.entities ?? {})) {
|
|
95
53
|
validateNoMultipleReferenceEdge({ name, entity }, feature.name, transferable);
|
|
96
54
|
}
|
|
97
|
-
|
|
98
|
-
// so the lookup is inverted here.
|
|
99
|
-
const childrenByParent = new Map();
|
|
100
|
-
for (const { name, entity } of allEntities(featureMap)) {
|
|
101
|
-
if (!transferable.has(name))
|
|
102
|
-
continue;
|
|
103
|
-
for (const target of referenceTargets(entity)) {
|
|
104
|
-
if (!transferable.has(target))
|
|
105
|
-
continue;
|
|
106
|
-
const children = childrenByParent.get(target) ?? [];
|
|
107
|
-
children.push(name);
|
|
108
|
-
childrenByParent.set(target, children);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
55
|
+
const adjacency = resolveTransferAdjacency(new Map(allEntities(featureMap).map(({ name, entity }) => [name, entity])));
|
|
111
56
|
for (const [name, entity] of Object.entries(feature.entities ?? {})) {
|
|
112
57
|
if (entity.transferable !== true)
|
|
113
58
|
continue;
|
|
114
|
-
const tooDeep =
|
|
59
|
+
const tooDeep = findOverDeepTransferChain(adjacency, [name]);
|
|
115
60
|
if (tooDeep !== undefined) {
|
|
116
61
|
throw new Error(`[Kumiko TransferGraph] the transfer graph rooted at entity "${name}" is deeper than ` +
|
|
117
62
|
`the ${MAX_TRANSFER_DEPTH}-level limit (feature: "${feature.name}"): ` +
|
|
118
63
|
`${tooDeep.join(" -> ")}. Beyond that depth tenant-handover would move fewer rows ` +
|
|
119
|
-
`than the declaration implies. Fix: flatten the chain, or
|
|
120
|
-
`entity in it that should not move with its host
|
|
64
|
+
`than the declaration implies. Fix: flatten the chain, or mark a reference field on ` +
|
|
65
|
+
`an entity in it that should not move with its host \`handover: "stay"\`.`);
|
|
121
66
|
}
|
|
122
67
|
}
|
|
123
68
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "../types/index.js";
|
|
2
2
|
export declare function collectUrlPrefillFieldsByScreenQn(features: readonly FeatureDefinition[]): Map<string, Set<string>>;
|
|
3
|
+
export declare function validateSubmitPrefilledHiddenFields(features: readonly FeatureDefinition[]): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
2
|
+
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
|
|
2
3
|
import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, } from "./screens.js";
|
|
3
4
|
function isNavigateKind(action) {
|
|
4
5
|
return action.kind === undefined || action.kind === "navigate";
|
|
@@ -123,3 +124,37 @@ function resolveTarget(source, screensByShortId, detailForByEntity) {
|
|
|
123
124
|
const match = screensByShortId.get(source.screen)?.[0];
|
|
124
125
|
return match === undefined ? undefined : { ...match, shortId: source.screen };
|
|
125
126
|
}
|
|
127
|
+
// submitPrefilled lets an unchanged form submit, so a required field the layout
|
|
128
|
+
// hides must be seeded by a navigate param — nothing else can ever fill it.
|
|
129
|
+
export function validateSubmitPrefilledHiddenFields(features) {
|
|
130
|
+
const prefillByScreenQn = collectUrlPrefillFieldsByScreenQn(features);
|
|
131
|
+
for (const feature of features) {
|
|
132
|
+
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
133
|
+
if (screen.type !== "actionForm" && screen.type !== "secretMint")
|
|
134
|
+
continue;
|
|
135
|
+
if (screen.submitPrefilled !== true)
|
|
136
|
+
continue;
|
|
137
|
+
const seeded = prefillByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
|
|
138
|
+
for (const section of screen.layout.sections) {
|
|
139
|
+
if (!isFieldsEditSection(section))
|
|
140
|
+
continue;
|
|
141
|
+
for (const rawSpec of sectionFieldSpecs(section)) {
|
|
142
|
+
const spec = normalizeEditField(rawSpec);
|
|
143
|
+
if (spec.visible !== false)
|
|
144
|
+
continue;
|
|
145
|
+
const fieldDef = screen.fields[spec.field];
|
|
146
|
+
if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (seeded?.has(spec.field) === true)
|
|
150
|
+
continue;
|
|
151
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
|
|
152
|
+
`hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
|
|
153
|
+
`params — the field could never get a value and the form could never submit. Add ` +
|
|
154
|
+
`"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
|
|
155
|
+
`visible, or drop required.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -459,6 +459,9 @@ function projectField(fieldDef) {
|
|
|
459
459
|
// Select: tier/tenant availability of the static options, loaded by the renderer.
|
|
460
460
|
if (typeof def["optionsAvailabilityQuery"] === "string")
|
|
461
461
|
out["optionsAvailabilityQuery"] = def["optionsAvailabilityQuery"];
|
|
462
|
+
// Select: options only offered while a sibling-field condition holds; the renderer filters, the write path enforces.
|
|
463
|
+
if (Array.isArray(def["conditionalOptions"]))
|
|
464
|
+
out["conditionalOptions"] = def["conditionalOptions"];
|
|
462
465
|
if (typeof def["multiple"] === "boolean")
|
|
463
466
|
out["multiple"] = def["multiple"];
|
|
464
467
|
// MultiSelect: display picks checkboxes vs. combobox in the renderer,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateChangelog } from "./feature-changelog.js";
|
|
2
2
|
const BLOCK_RE = /<!--\s*kumiko-changes\s*\n([\s\S]*?)\n\s*-->/g;
|
|
3
3
|
const TYPE_VALUES = new Set(["breaking", "improvement", "fix"]);
|
|
4
|
-
const KEY_RE = /^(feature|type|title|detail|migration|codemod):(?:\s(.*))?$/;
|
|
4
|
+
const KEY_RE = /^(feature|type|title|detail|migration|codemod|manualAfterCodemod):(?:\s(.*))?$/;
|
|
5
5
|
function requiredField(fields, field, source) {
|
|
6
6
|
const value = fields.get(field)?.trim();
|
|
7
7
|
if (!value)
|
|
@@ -70,6 +70,53 @@ function assertBlockNotClosedEarly(markdown, match, source) {
|
|
|
70
70
|
throw new Error(`${source}: kumiko-changes block closed early by a line starting with "-->"; field values must not contain such a line`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
function resolveTitleAndDetail(fields, prose, source) {
|
|
74
|
+
const explicitTitle = fields.get("title")?.trim();
|
|
75
|
+
const title = explicitTitle || prose[0];
|
|
76
|
+
if (!title)
|
|
77
|
+
throw new Error(`${source}: kumiko-changes title is required`);
|
|
78
|
+
// The first prose line is the title's source only when no title was given or
|
|
79
|
+
// the explicit one restates it; otherwise it is genuine detail.
|
|
80
|
+
const firstLineIsTitle = !explicitTitle || sameTitle(explicitTitle, prose[0]);
|
|
81
|
+
const detail = fields.get("detail")?.trim() ||
|
|
82
|
+
prose
|
|
83
|
+
.slice(firstLineIsTitle ? 1 : 0)
|
|
84
|
+
.join("\n")
|
|
85
|
+
.trim();
|
|
86
|
+
return { title, detail };
|
|
87
|
+
}
|
|
88
|
+
function parseManualAfterCodemod(fields, source) {
|
|
89
|
+
const raw = fields.get("manualAfterCodemod")?.trim();
|
|
90
|
+
if (raw !== undefined && raw !== "true" && raw !== "false") {
|
|
91
|
+
throw new Error(`${source}: kumiko-changes manualAfterCodemod must be true or false`);
|
|
92
|
+
}
|
|
93
|
+
return raw === "true";
|
|
94
|
+
}
|
|
95
|
+
function buildChange(fields, prose, source) {
|
|
96
|
+
const feature = requiredField(fields, "feature", source);
|
|
97
|
+
const rawType = requiredField(fields, "type", source);
|
|
98
|
+
if (!TYPE_VALUES.has(rawType)) {
|
|
99
|
+
throw new Error(`${source}: kumiko-changes type must be breaking, improvement, or fix`);
|
|
100
|
+
}
|
|
101
|
+
const { title, detail } = resolveTitleAndDetail(fields, prose, source);
|
|
102
|
+
const migration = fields.get("migration")?.trim();
|
|
103
|
+
const codemod = fields.get("codemod")?.trim();
|
|
104
|
+
const manualAfterCodemod = parseManualAfterCodemod(fields, source);
|
|
105
|
+
const change = {
|
|
106
|
+
feature,
|
|
107
|
+
type: rawType,
|
|
108
|
+
title,
|
|
109
|
+
...(detail ? { detail } : {}),
|
|
110
|
+
...(migration ? { migration } : {}),
|
|
111
|
+
...(codemod ? { codemod } : {}),
|
|
112
|
+
...(manualAfterCodemod ? { manualAfterCodemod: true } : {}),
|
|
113
|
+
source,
|
|
114
|
+
};
|
|
115
|
+
const validation = validateChangelog({ version: "0.0.0", ...change });
|
|
116
|
+
if (validation.length > 0)
|
|
117
|
+
throw new Error(`${source}: ${validation.join("; ")}`);
|
|
118
|
+
return change;
|
|
119
|
+
}
|
|
73
120
|
/** Parse structured upgrade metadata embedded in a Changeset body. */
|
|
74
121
|
export function parseChangesetChanges(markdown, source) {
|
|
75
122
|
const changes = [];
|
|
@@ -85,38 +132,7 @@ export function parseChangesetChanges(markdown, source) {
|
|
|
85
132
|
assertBlockNotClosedEarly(markdown, match, source);
|
|
86
133
|
if (!block)
|
|
87
134
|
throw new Error(`${source}: empty kumiko-changes block`);
|
|
88
|
-
const
|
|
89
|
-
const feature = requiredField(fields, "feature", source);
|
|
90
|
-
const rawType = requiredField(fields, "type", source);
|
|
91
|
-
if (!TYPE_VALUES.has(rawType)) {
|
|
92
|
-
throw new Error(`${source}: kumiko-changes type must be breaking, improvement, or fix`);
|
|
93
|
-
}
|
|
94
|
-
const explicitTitle = fields.get("title")?.trim();
|
|
95
|
-
const title = explicitTitle || prose[0];
|
|
96
|
-
if (!title)
|
|
97
|
-
throw new Error(`${source}: kumiko-changes title is required`);
|
|
98
|
-
// The first prose line is the title's source only when no title was given or
|
|
99
|
-
// the explicit one restates it; otherwise it is genuine detail.
|
|
100
|
-
const firstLineIsTitle = !explicitTitle || sameTitle(explicitTitle, prose[0]);
|
|
101
|
-
const detail = fields.get("detail")?.trim() ||
|
|
102
|
-
prose
|
|
103
|
-
.slice(firstLineIsTitle ? 1 : 0)
|
|
104
|
-
.join("\n")
|
|
105
|
-
.trim();
|
|
106
|
-
const migration = fields.get("migration")?.trim();
|
|
107
|
-
const codemod = fields.get("codemod")?.trim();
|
|
108
|
-
const change = {
|
|
109
|
-
feature,
|
|
110
|
-
type: rawType,
|
|
111
|
-
title,
|
|
112
|
-
...(detail ? { detail } : {}),
|
|
113
|
-
...(migration ? { migration } : {}),
|
|
114
|
-
...(codemod ? { codemod } : {}),
|
|
115
|
-
source,
|
|
116
|
-
};
|
|
117
|
-
const validation = validateChangelog({ version: "0.0.0", ...change });
|
|
118
|
-
if (validation.length > 0)
|
|
119
|
-
throw new Error(`${source}: ${validation.join("; ")}`);
|
|
135
|
+
const change = buildChange(parseBlock(block, source), prose, source);
|
|
120
136
|
changes.push(change);
|
|
121
137
|
match = BLOCK_RE.exec(markdown);
|
|
122
138
|
}
|
|
@@ -22,7 +22,18 @@ import { unwrapArrayForm } from "./object-form.js";
|
|
|
22
22
|
function resolveFeatureNamesArgs(args) {
|
|
23
23
|
return unwrapArrayForm(args, "features");
|
|
24
24
|
}
|
|
25
|
+
// dedupe-features compares options with Object.is per key, so a nested object
|
|
26
|
+
// would make two structurally identical mounts look different and fail the boot
|
|
27
|
+
// with an unobvious "mounted twice with different options".
|
|
28
|
+
function assertDedupeOptionsComparable(featureName, options) {
|
|
29
|
+
for (const [key, value] of Object.entries(options ?? {})) {
|
|
30
|
+
if (typeof value === "object" && value !== null) {
|
|
31
|
+
throw new Error(`[Feature ${featureName}] dedupeOptions.${key} must be a primitive or a stable function reference; dedupeOptions are compared shallowly, so a nested object never matches a second mount`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
25
35
|
export function defineFeature(name, setup, settings) {
|
|
36
|
+
assertDedupeOptionsComparable(name, settings?.dedupeOptions);
|
|
26
37
|
const state = createInitialFeatureBuilderState();
|
|
27
38
|
const registrar = {
|
|
28
39
|
systemScope() {
|
|
@@ -4,8 +4,6 @@ import { type EntityTable } from "../db/table-builder.js";
|
|
|
4
4
|
import type { EntityCrudRegistrar, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions } from "./types/entity-handlers.js";
|
|
5
5
|
import type { AccessRule, EntityDefinition, EscapeHatchDeclaration, QueryHandlerDef, WriteHandlerDef } from "./types/index.js";
|
|
6
6
|
export type { EntityCrudHandlerDefaults, EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "./types/entity-handlers.js";
|
|
7
|
-
export declare function isDeprecatedCrossTenantHandler(handler: object): boolean;
|
|
8
|
-
export declare const DEPRECATED_CROSS_TENANT_SIGNAL = "deprecation:entity-handler-cross-tenant";
|
|
9
7
|
export declare const MAX_LIST_LIMIT = 200;
|
|
10
8
|
export declare const entityListSchema: z.ZodObject<{
|
|
11
9
|
cursor: z.ZodOptional<z.ZodString>;
|
|
@@ -60,7 +58,9 @@ export declare function createEntityExecutor(entityName: string, entity: EntityD
|
|
|
60
58
|
};
|
|
61
59
|
export declare function defineProjectionQueryHandler(name: string, projectionQualifiedName: string, options: {
|
|
62
60
|
access: AccessRule;
|
|
63
|
-
unsafeAllTenants?: boolean
|
|
61
|
+
unsafeAllTenants?: boolean | {
|
|
62
|
+
readonly reason: string;
|
|
63
|
+
};
|
|
64
64
|
escapeHatch?: EscapeHatchDeclaration;
|
|
65
65
|
}): QueryHandlerDef;
|
|
66
66
|
/** Register standard entity CRUD handlers in one call. Access stays explicit — no openToAll default. */
|