@cosmicdrift/kumiko-framework 0.360.0 → 0.362.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +14 -1
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.d.ts +2 -0
- package/dist/crypto/owner-transition-rekey.js +30 -0
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +5 -0
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +14 -1
- package/dist/db/apply-entity-event.js +0 -5
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-field-encryption.js +3 -2
- package/dist/db/entity-table-meta.js +3 -3
- package/dist/db/event-store-executor-context.d.ts +1 -0
- package/dist/db/event-store-executor-context.js +9 -2
- package/dist/db/event-store-executor-write.js +31 -5
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/event-store-executor.js +1 -0
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.js +2 -2
- package/dist/db/migrate-generator.d.ts +1 -0
- package/dist/db/migrate-generator.js +12 -0
- package/dist/db/queries/backfill-pii.d.ts +2 -0
- package/dist/db/queries/backfill-pii.js +120 -15
- package/dist/db/table-builder.d.ts +3 -1
- package/dist/db/table-builder.js +3 -6
- package/dist/engine/boot-validator/access-declarations.js +29 -18
- package/dist/engine/boot-validator/card-meta-overflow.js +5 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +38 -1
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/query-output-columns.js +3 -0
- package/dist/engine/boot-validator/screens.d.ts +1 -1
- package/dist/engine/boot-validator/screens.js +89 -14
- package/dist/engine/build-app-schema.js +1 -0
- package/dist/engine/define-handler.js +1 -3
- package/dist/engine/factories.d.ts +2 -0
- package/dist/engine/feature-ast/extractors/round3.d.ts +2 -2
- package/dist/engine/feature-ast/extractors/round3.js +4 -4
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/schema-builder.js +2 -2
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +9 -0
- package/dist/engine/soft-delete-cleanup.js +12 -1
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/errors/classes.d.ts +7 -0
- package/dist/errors/classes.js +12 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +8 -0
- package/dist/files/assert-file-refs-belong-to-tenant.js +50 -0
- package/dist/files/bind-pending-file-refs.d.ts +1 -0
- package/dist/files/bind-pending-file-refs.js +13 -13
- package/dist/files/bind-referenced-files.write.d.ts +16 -0
- package/dist/files/bind-referenced-files.write.js +109 -0
- package/dist/files/dispose-removed-file-refs.d.ts +8 -0
- package/dist/files/dispose-removed-file-refs.js +51 -0
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -0
- package/dist/files/file-ref-entity.d.ts +1 -1
- package/dist/files/file-ref-entity.js +1 -1
- package/dist/files/file-routes.d.ts +4 -1
- package/dist/files/file-routes.js +37 -6
- package/dist/files/purge-expired-file-ref-bytes.d.ts +14 -0
- package/dist/files/purge-expired-file-ref-bytes.js +34 -0
- package/dist/files/record-write-check.d.ts +22 -0
- package/dist/files/record-write-check.js +64 -0
- package/dist/files/types.d.ts +1 -0
- package/dist/files/types.js +3 -0
- package/dist/i18n/required-surface-keys.js +10 -0
- package/dist/migrations/pii-event-backfill.d.ts +1 -0
- package/dist/migrations/pii-event-backfill.js +16 -2
- package/dist/pipeline/cascade-handler.js +17 -2
- package/dist/pipeline/dispatch-write.js +6 -6
- package/dist/pipeline/write-origin.js +2 -2
- package/dist/schema-cli.js +12 -6
- package/dist/search/reindex-entity.js +3 -4
- package/dist/stack/test-stack.d.ts +3 -1
- package/dist/stack/test-stack.js +1 -0
- package/dist/ui-types/card-column-roles.d.ts +5 -0
- package/dist/ui-types/card-column-roles.js +22 -4
- package/dist/ui-types/index.d.ts +3 -3
- package/dist/ui-types/index.js +2 -2
- package/package.json +4 -4
- package/src/changes.json +134 -0
|
@@ -32,6 +32,44 @@ function validateAtMostOneRowClick(featureName, screenId, screenType, rowActions
|
|
|
32
32
|
// literal `required: true` (screen-spec override or entity-level default).
|
|
33
33
|
// A dynamic FieldCondition depends on runtime form values and can't be
|
|
34
34
|
// evaluated at boot; buildFormSchema() silently skips presence-checking it.
|
|
35
|
+
// `embeddedList` and `prefill` only make sense on field types that support them;
|
|
36
|
+
// a misplaced spec would otherwise be a silent no-op.
|
|
37
|
+
function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
|
|
38
|
+
const fieldDef = entityDef.fields[fieldSpec.field];
|
|
39
|
+
// skip: the field name is not on the entity, other checks report unknown fields
|
|
40
|
+
if (fieldDef === undefined)
|
|
41
|
+
return;
|
|
42
|
+
const where = `[Feature ${featureName}] Screen "${screenId}" (entityEdit) field "${fieldSpec.field}"`;
|
|
43
|
+
const listSpec = fieldSpec.embeddedList;
|
|
44
|
+
if (listSpec !== undefined) {
|
|
45
|
+
if (fieldDef.type !== "embedded" || fieldDef.multiple !== true) {
|
|
46
|
+
throw new Error(`${where} sets embeddedList, which only applies to an embedded list ` +
|
|
47
|
+
`(createEmbeddedListField); the field is type "${fieldDef.type}".`);
|
|
48
|
+
}
|
|
49
|
+
for (const cell of listSpec.readOnlyCells ?? []) {
|
|
50
|
+
if (fieldDef.schema[cell] === undefined) {
|
|
51
|
+
throw new Error(`${where} embeddedList.readOnlyCells names "${cell}", which is not a sub-field ` +
|
|
52
|
+
`(known: ${Object.keys(fieldDef.schema).sort().join(", ")}).`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const prefill = fieldSpec.prefill;
|
|
57
|
+
// skip: no prefill declared
|
|
58
|
+
if (prefill === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (prefill.kind === "today") {
|
|
61
|
+
if (fieldDef.type !== "date") {
|
|
62
|
+
throw new Error(`${where} prefill "today" needs a date field; the field is type "${fieldDef.type}".`);
|
|
63
|
+
}
|
|
64
|
+
// skip: today carries no property, nothing further to check
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const allowsUserId = fieldDef.type === "text" || fieldDef.type === "reference";
|
|
68
|
+
if (prefill.property === "id" ? !allowsUserId : fieldDef.type !== "text") {
|
|
69
|
+
throw new Error(`${where} prefill currentUser.${prefill.property} does not fit a field of type ` +
|
|
70
|
+
`"${fieldDef.type}" (displayName and email need text; id needs text or a reference).`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
35
73
|
function validateNoWidgetRequiredField(featureName, screenId, entityDef, fieldSpec) {
|
|
36
74
|
const fieldDef = entityDef.fields[fieldSpec.field];
|
|
37
75
|
// skip: field doesn't exist or its type already has a widget — nothing to validate.
|
|
@@ -648,7 +686,8 @@ function validateFormLayoutSections(featureName, screenId, context, layout, fiel
|
|
|
648
686
|
// form (fw#2670) carries the same target under `screen` plus the payload
|
|
649
687
|
// field `idFrom`. Shared by actionForm/secretMint (validateInlineFormNavTargets)
|
|
650
688
|
// and entityEdit — same rule, same error message, one place to keep them in sync.
|
|
651
|
-
function validateRedirectTarget(feature, screenId, screenKind, redirect,
|
|
689
|
+
function validateRedirectTarget(feature, screenId, screenKind, redirect, lookups) {
|
|
690
|
+
const { allScreenQns } = lookups;
|
|
652
691
|
const redirectTarget = typeof redirect === "string" ? redirect : redirect.screen;
|
|
653
692
|
if (typeof redirect !== "string" &&
|
|
654
693
|
(typeof redirect.idFrom !== "string" || redirect.idFrom.trim() === "")) {
|
|
@@ -674,6 +713,13 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, allScre
|
|
|
674
713
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screenKind}) redirect.idFrom is set but target screen ` +
|
|
675
714
|
`"${redirectTarget}" (${sameFeatureTarget.type}) carries no id — use the plain string form.`);
|
|
676
715
|
}
|
|
716
|
+
if (typeof redirect !== "string" && redirect.tab !== undefined) {
|
|
717
|
+
const targetQn = resolveScreenTargetQn(feature.name, redirectTarget);
|
|
718
|
+
const target = lookups.screensByShortId
|
|
719
|
+
.get(targetQn.slice(targetQn.lastIndexOf(":") + 1))
|
|
720
|
+
?.find((entry) => qualifyEntityName(entry.featureName, "screen", entry.screen.id) === targetQn);
|
|
721
|
+
validateRowActionNavigateTab(feature.name, screenId, screenKind, "redirect", { kind: "navigate", id: redirectTarget, label: redirectTarget, tab: redirect.tab }, target);
|
|
722
|
+
}
|
|
677
723
|
}
|
|
678
724
|
// redirect is only honored on record actions; on list/related-list row
|
|
679
725
|
// actions it would be silently dropped, so the author would believe it works.
|
|
@@ -686,9 +732,25 @@ function rejectRedirectOnListRowAction(featureName, screenId, screenKind, action
|
|
|
686
732
|
}
|
|
687
733
|
// A writeHandler record action's redirect (projectionDetail/entityEdit
|
|
688
734
|
// header and section actions) follows the entityEdit.redirect rule.
|
|
689
|
-
function validateRecordActionRedirect(feature, screenId, screenKind, action,
|
|
735
|
+
function validateRecordActionRedirect(feature, screenId, screenKind, action, lookups) {
|
|
690
736
|
if (action.kind !== "navigate" && action.kind !== "drawer" && action.redirect !== undefined) {
|
|
691
|
-
validateRedirectTarget(feature, screenId, screenKind, action.redirect,
|
|
737
|
+
validateRedirectTarget(feature, screenId, screenKind, action.redirect, lookups);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// "edit" is the default edit action, which only exists when detailFor names
|
|
741
|
+
// the entity; a typo would otherwise silently fall back to the default precedence.
|
|
742
|
+
function validatePrimaryActionTarget(featureName, screenId, screen) {
|
|
743
|
+
// skip: no declared primaryAction, the default header precedence applies
|
|
744
|
+
if (screen.primaryAction === undefined)
|
|
745
|
+
return;
|
|
746
|
+
const declaredIds = (screen.actions ?? []).map((action) => action.id);
|
|
747
|
+
const editIsAvailable = screen.detailFor !== undefined;
|
|
748
|
+
const targetExists = declaredIds.includes(screen.primaryAction) ||
|
|
749
|
+
(editIsAvailable && screen.primaryAction === "edit");
|
|
750
|
+
if (!targetExists) {
|
|
751
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) primaryAction "${screen.primaryAction}" ` +
|
|
752
|
+
`matches no action — use one of the declared action ids (${declaredIds.join(", ") || "none declared"})` +
|
|
753
|
+
`${editIsAvailable ? ' or "edit"' : ' ("edit" needs detailFor on this screen)'}.`);
|
|
692
754
|
}
|
|
693
755
|
}
|
|
694
756
|
function validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, featureMap) {
|
|
@@ -718,9 +780,10 @@ function validateScreenFallback(feature, screenId, screenType, fallback, allScre
|
|
|
718
780
|
throw new Error(`${context} fallback "${fallback}" has its own visibleWhen; a fallback must always render.`);
|
|
719
781
|
}
|
|
720
782
|
}
|
|
721
|
-
function validateInlineFormNavTargets(feature, screenId, screen, kind,
|
|
783
|
+
function validateInlineFormNavTargets(feature, screenId, screen, kind, lookups) {
|
|
784
|
+
const { allScreenQns } = lookups;
|
|
722
785
|
if (screen.redirect !== undefined) {
|
|
723
|
-
validateRedirectTarget(feature, screenId, kind, screen.redirect,
|
|
786
|
+
validateRedirectTarget(feature, screenId, kind, screen.redirect, lookups);
|
|
724
787
|
}
|
|
725
788
|
if (typeof screen.cancelTarget === "string") {
|
|
726
789
|
// Same rule as redirect — `false` (no Cancel button) needs no validation.
|
|
@@ -729,7 +792,7 @@ function validateInlineFormNavTargets(feature, screenId, screen, kind, allScreen
|
|
|
729
792
|
}
|
|
730
793
|
// actionForm/secretMint have no entity link, only a write-handler QN +
|
|
731
794
|
// inline fields: checks handler registration, field types, layout refs, and nav targets.
|
|
732
|
-
function validateInlineFormScreen(feature, screenId, screen, kind, allWriteHandlerQns,
|
|
795
|
+
function validateInlineFormScreen(feature, screenId, screen, kind, allWriteHandlerQns, lookups, featureMap) {
|
|
733
796
|
validateWriteHandlerRegistered(feature.name, screenId, kind, screen.handler, allWriteHandlerQns);
|
|
734
797
|
// Only a secretMint's own mint step may skip fields/sections entirely
|
|
735
798
|
// (input-less mint, fw#2838) — actionForm always needs at least one field.
|
|
@@ -737,7 +800,7 @@ function validateInlineFormScreen(feature, screenId, screen, kind, allWriteHandl
|
|
|
737
800
|
const fieldNames = validateFormFieldsMap(feature.name, screenId, kind, screen.fields, allowEmptyMintForm);
|
|
738
801
|
validateFormLayoutSections(feature.name, screenId, kind, screen.layout, fieldNames, allowEmptyMintForm && fieldNames.size === 0);
|
|
739
802
|
validateWizardLayout(feature.name, screenId, kind, screen.layout, featureMap);
|
|
740
|
-
validateInlineFormNavTargets(feature, screenId, screen, kind,
|
|
803
|
+
validateInlineFormNavTargets(feature, screenId, screen, kind, lookups);
|
|
741
804
|
}
|
|
742
805
|
// SecretMintConfirmStep (fw#2838): an optional proof-of-receipt form rendered
|
|
743
806
|
// on the reveal card in place of the bare acknowledge button (TOTP enrollment:
|
|
@@ -965,6 +1028,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
965
1028
|
};
|
|
966
1029
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
967
1030
|
validateExplicitListScreenExists(feature.name, screenId, screen, screensByShortId);
|
|
1031
|
+
if ((screen.type === "entityList" || screen.type === "projectionList") &&
|
|
1032
|
+
screen.emptyState?.action !== undefined) {
|
|
1033
|
+
validateActionHasIcon(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
|
|
1034
|
+
validateNavigateActionTab(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action, navigateLookups);
|
|
1035
|
+
}
|
|
968
1036
|
// Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
|
|
969
1037
|
if ((screen.type === "entityEdit" ||
|
|
970
1038
|
screen.type === "actionForm" ||
|
|
@@ -1169,7 +1237,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1169
1237
|
if (section.actions !== undefined) {
|
|
1170
1238
|
for (const action of section.actions) {
|
|
1171
1239
|
validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" action`, action);
|
|
1172
|
-
validateRecordActionRedirect(feature, screenId, "projectionDetail", action,
|
|
1240
|
+
validateRecordActionRedirect(feature, screenId, "projectionDetail", action, navigateLookups);
|
|
1173
1241
|
validateNavigateActionTab(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" action`, action, navigateLookups);
|
|
1174
1242
|
}
|
|
1175
1243
|
}
|
|
@@ -1277,10 +1345,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1277
1345
|
// the row), so the same navigate/writeHandler existence checks as
|
|
1278
1346
|
// entityList/projectionList apply. `rowClick` is rejected outright —
|
|
1279
1347
|
// a detail screen has no row to click.
|
|
1348
|
+
validatePrimaryActionTarget(feature.name, screenId, screen);
|
|
1280
1349
|
if (screen.actions !== undefined) {
|
|
1281
1350
|
for (const action of screen.actions) {
|
|
1282
1351
|
validateActionHasIcon(feature.name, screenId, "projectionDetail", "action", action);
|
|
1283
|
-
validateRecordActionRedirect(feature, screenId, "projectionDetail", action,
|
|
1352
|
+
validateRecordActionRedirect(feature, screenId, "projectionDetail", action, navigateLookups);
|
|
1284
1353
|
if (action.kind === "navigate" && action.rowClick === true) {
|
|
1285
1354
|
throw new Error(`[Feature ${feature.name}] Screen "${qualifyEntityName(feature.name, "screen", screenId)}" ` +
|
|
1286
1355
|
`(projectionDetail) action "${action.id}" sets rowClick: true — there is no row to ` +
|
|
@@ -1381,11 +1450,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1381
1450
|
continue;
|
|
1382
1451
|
}
|
|
1383
1452
|
if (screen.type === "actionForm") {
|
|
1384
|
-
validateInlineFormScreen(feature, screenId, screen, "actionForm", allWriteHandlerQns,
|
|
1453
|
+
validateInlineFormScreen(feature, screenId, screen, "actionForm", allWriteHandlerQns, navigateLookups, featureMap);
|
|
1385
1454
|
continue;
|
|
1386
1455
|
}
|
|
1387
1456
|
if (screen.type === "secretMint") {
|
|
1388
|
-
validateInlineFormScreen(feature, screenId, screen, "secretMint", allWriteHandlerQns,
|
|
1457
|
+
validateInlineFormScreen(feature, screenId, screen, "secretMint", allWriteHandlerQns, navigateLookups, featureMap);
|
|
1389
1458
|
if (screen.reveal.fields.length === 0) {
|
|
1390
1459
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has an empty reveal.fields ` +
|
|
1391
1460
|
`list — declare at least one field to reveal.`);
|
|
@@ -1663,7 +1732,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1663
1732
|
const sectionLabel = section.title ?? section.id ?? "(untitled)";
|
|
1664
1733
|
for (const action of section.actions) {
|
|
1665
1734
|
validateActionHasIcon(feature.name, screenId, "entityEdit", `section "${sectionLabel}" action`, action);
|
|
1666
|
-
validateRecordActionRedirect(feature, screenId, "entityEdit", action,
|
|
1735
|
+
validateRecordActionRedirect(feature, screenId, "entityEdit", action, navigateLookups);
|
|
1667
1736
|
validateNavigateActionTab(feature.name, screenId, "entityEdit", `section "${sectionLabel}" action`, action, navigateLookups);
|
|
1668
1737
|
}
|
|
1669
1738
|
}
|
|
@@ -1690,6 +1759,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1690
1759
|
throw new Error(buildUnknownFieldMessage(feature.name, screenId, normalized.field, screen.entity, fieldNames));
|
|
1691
1760
|
}
|
|
1692
1761
|
validateNoWidgetRequiredField(feature.name, screenId, entityDef, normalized);
|
|
1762
|
+
validateEditFieldExtras(feature.name, screenId, entityDef, normalized);
|
|
1693
1763
|
}
|
|
1694
1764
|
}
|
|
1695
1765
|
validateWizardLayout(feature.name, screenId, "entityEdit", screen.layout, featureMap);
|
|
@@ -1699,7 +1769,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1699
1769
|
// row to click either.
|
|
1700
1770
|
if (screen.actions !== undefined) {
|
|
1701
1771
|
for (const action of screen.actions) {
|
|
1702
|
-
validateRecordActionRedirect(feature, screenId, "entityEdit", action,
|
|
1772
|
+
validateRecordActionRedirect(feature, screenId, "entityEdit", action, navigateLookups);
|
|
1703
1773
|
if (action.kind === "navigate" && action.rowClick === true) {
|
|
1704
1774
|
throw new Error(`[Feature ${feature.name}] Screen "${qualifyEntityName(feature.name, "screen", screenId)}" ` +
|
|
1705
1775
|
`(entityEdit) action "${action.id}" sets rowClick: true — there is no row to click ` +
|
|
@@ -1726,8 +1796,13 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1726
1796
|
`"${screen.recordTitleField}" is not a field of entity "${screen.entity}" ` +
|
|
1727
1797
|
`(known: ${[...fieldNames].sort().join(", ")}).`);
|
|
1728
1798
|
}
|
|
1799
|
+
if (typeof screen.readOnly === "object" && !fieldNames.has(screen.readOnly.field)) {
|
|
1800
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) readOnly condition field ` +
|
|
1801
|
+
`"${screen.readOnly.field}" is not a field of entity "${screen.entity}" ` +
|
|
1802
|
+
`(known: ${[...fieldNames].sort().join(", ")}).`);
|
|
1803
|
+
}
|
|
1729
1804
|
if (screen.redirect !== undefined) {
|
|
1730
|
-
validateRedirectTarget(feature, screenId, "entityEdit", screen.redirect,
|
|
1805
|
+
validateRedirectTarget(feature, screenId, "entityEdit", screen.redirect, navigateLookups);
|
|
1731
1806
|
}
|
|
1732
1807
|
}
|
|
1733
1808
|
}
|
|
@@ -409,6 +409,7 @@ function projectEntity(entityName, entity) {
|
|
|
409
409
|
...(typeof entity.defaultCurrency === "string" && {
|
|
410
410
|
defaultCurrency: entity.defaultCurrency,
|
|
411
411
|
}),
|
|
412
|
+
...(entity.lockedWhen !== undefined && { lockedWhen: entity.lockedWhen }),
|
|
412
413
|
};
|
|
413
414
|
}
|
|
414
415
|
// Nur valueType durch — die derive-fn ist Server-only und NICHT JSON-safe
|
|
@@ -46,9 +46,7 @@ export function defineWriteHandler(def,
|
|
|
46
46
|
...(def.access && { access: def.access }),
|
|
47
47
|
...(def.description !== undefined && { description: def.description }),
|
|
48
48
|
...(def.agent && { agent: def.agent }),
|
|
49
|
-
...(def.unsafeSkipTransitionGuard && {
|
|
50
|
-
unsafeSkipTransitionGuard: def.unsafeSkipTransitionGuard,
|
|
51
|
-
}),
|
|
49
|
+
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
52
50
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
53
51
|
...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
|
|
54
52
|
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
@@ -184,6 +184,8 @@ export declare function createEntity<F, const T extends EntityTenancy = "tenant"
|
|
|
184
184
|
readonly searchWeight?: number;
|
|
185
185
|
readonly defaultCurrency?: string;
|
|
186
186
|
readonly transitions?: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;
|
|
187
|
+
/** See EntityDefinition.lockedWhen. */
|
|
188
|
+
readonly lockedWhen?: EntityDefinition["lockedWhen"];
|
|
187
189
|
readonly indexes?: readonly EntityIndexDef[];
|
|
188
190
|
readonly idType?: "serial" | "uuid";
|
|
189
191
|
readonly access?: EntityDefinition["access"];
|
|
@@ -2,7 +2,7 @@ import type { CallExpression, SourceFile } from "ts-morph";
|
|
|
2
2
|
import type { ClaimKeyType } from "../../types/handlers.js";
|
|
3
3
|
import type { ParseError } from "../parse.js";
|
|
4
4
|
import type { ClaimKeyPattern, ConfigPattern, MetricPattern, ReferenceDataPattern, SecretNamespacePattern, SecretPattern, TranslationsPattern, UseExtensionPattern } from "../patterns.js";
|
|
5
|
-
import { type ExtractOutput } from "./shared.js";
|
|
5
|
+
import { type ExtractOutput, type ReadDataLiteralOptions } from "./shared.js";
|
|
6
6
|
export type NamedOptionsResult = {
|
|
7
7
|
readonly kind: "ok";
|
|
8
8
|
readonly name: string;
|
|
@@ -11,7 +11,7 @@ export type NamedOptionsResult = {
|
|
|
11
11
|
readonly kind: "error";
|
|
12
12
|
readonly error: ParseError;
|
|
13
13
|
};
|
|
14
|
-
export declare function readNamedOptions(call: CallExpression, sourceFile: SourceFile, methodName: string): NamedOptionsResult;
|
|
14
|
+
export declare function readNamedOptions(call: CallExpression, sourceFile: SourceFile, methodName: string, readOptions?: ReadDataLiteralOptions): NamedOptionsResult;
|
|
15
15
|
export declare function extractConfig(call: CallExpression, sourceFile: SourceFile): ExtractOutput<ConfigPattern>;
|
|
16
16
|
export declare function extractTranslations(call: CallExpression, sourceFile: SourceFile): ExtractOutput<TranslationsPattern>;
|
|
17
17
|
export declare function extractMetric(call: CallExpression, sourceFile: SourceFile): ExtractOutput<MetricPattern>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SyntaxKind } from "ts-morph";
|
|
2
2
|
import { sourceLocationFromNode } from "../source-location.js";
|
|
3
3
|
import { fail, isPlainObject, ok, readDataLiteralNode, readNameLiteral, readNameLiteralRef, readNameOrRef, } from "./shared.js";
|
|
4
|
-
export function readNamedOptions(call, sourceFile, methodName) {
|
|
4
|
+
export function readNamedOptions(call, sourceFile, methodName, readOptions = {}) {
|
|
5
5
|
const args = call.getArguments();
|
|
6
6
|
const first = args[0];
|
|
7
7
|
if (!first) {
|
|
@@ -17,7 +17,7 @@ export function readNamedOptions(call, sourceFile, methodName) {
|
|
|
17
17
|
if (!nameInit) {
|
|
18
18
|
return fail(methodName, sourceLocationFromNode(call, sourceFile), "object form requires a string-literal `name` property");
|
|
19
19
|
}
|
|
20
|
-
const data = readDataLiteralNode(obj);
|
|
20
|
+
const data = readDataLiteralNode(obj, readOptions);
|
|
21
21
|
if (!isPlainObject(data)) {
|
|
22
22
|
return fail(methodName, sourceLocationFromNode(call, sourceFile), "argument could not be read as a plain object");
|
|
23
23
|
}
|
|
@@ -32,7 +32,7 @@ export function readNamedOptions(call, sourceFile, methodName) {
|
|
|
32
32
|
if (!optionsArg) {
|
|
33
33
|
return fail(methodName, sourceLocationFromNode(call, sourceFile), "expected an options object as second argument");
|
|
34
34
|
}
|
|
35
|
-
const options = readDataLiteralNode(optionsArg);
|
|
35
|
+
const options = readDataLiteralNode(optionsArg, readOptions);
|
|
36
36
|
if (!isPlainObject(options)) {
|
|
37
37
|
return fail(methodName, sourceLocationFromNode(call, sourceFile), "options could not be read as a plain object");
|
|
38
38
|
}
|
|
@@ -88,7 +88,7 @@ export function extractMetric(call, sourceFile) {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
export function extractSecret(call, sourceFile) {
|
|
91
|
-
const parsed = readNamedOptions(call, sourceFile, "secret");
|
|
91
|
+
const parsed = readNamedOptions(call, sourceFile, "secret", { functionValuesAsRaw: true });
|
|
92
92
|
if (parsed.kind === "error")
|
|
93
93
|
return parsed;
|
|
94
94
|
return ok({
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUse
|
|
|
80
80
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
81
81
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
|
|
82
82
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
83
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, 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, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, 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, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, 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, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, 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.js";
|
|
83
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, 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, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, 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, 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, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, 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, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, 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.js";
|
|
84
84
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
85
85
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
86
86
|
export { runValidation } from "./validation.js";
|
|
@@ -409,8 +409,8 @@ export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES],
|
|
|
409
409
|
// Field types whose column is nullable and whose projection write accepts a
|
|
410
410
|
// plain `null`. Excluded: select/writeOnly text (fieldToZod is already
|
|
411
411
|
// nullable there), and types where null would not clear cleanly — NOT NULL
|
|
412
|
-
// jsonb columns (multiSelect, embedded, jsonb, multi reference
|
|
413
|
-
//
|
|
412
|
+
// jsonb columns (multiSelect, embedded, jsonb, multi reference, files/images),
|
|
413
|
+
// locatedTimestamp (split into _utc/_tz
|
|
414
414
|
// columns, a bare null targets a column that does not exist) and money (the
|
|
415
415
|
// currency column would keep its stale value).
|
|
416
416
|
const NULL_CLEARABLE_FIELD_TYPES = new Set([
|
|
@@ -25,6 +25,7 @@ export declare function normalizeListColumn(c: ListColumnSpec): Exclude<ListColu
|
|
|
25
25
|
* (form-condition closures) reuse it; three hand-rolled copies had
|
|
26
26
|
* already drifted in shape. */
|
|
27
27
|
export declare function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean;
|
|
28
|
+
export declare function isRecordLocked(lockedWhen: FieldCondition | undefined, row: Readonly<Record<string, unknown>>): boolean;
|
|
28
29
|
type SelectOptionRules = Pick<SelectFieldDef, "options" | "conditionalOptions">;
|
|
29
30
|
export type UnavailableSelectOption = {
|
|
30
31
|
readonly field: string;
|
|
@@ -110,6 +110,15 @@ export function evalFieldCondition(cond, values) {
|
|
|
110
110
|
return !cond.notIn.includes(val);
|
|
111
111
|
return val !== cond.ne;
|
|
112
112
|
}
|
|
113
|
+
// A row that lacks the condition's field counts as locked: an unreadable lock state
|
|
114
|
+
// must not open the record.
|
|
115
|
+
export function isRecordLocked(lockedWhen, row) {
|
|
116
|
+
if (lockedWhen === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
if (typeof lockedWhen === "object" && !(lockedWhen.field in row))
|
|
119
|
+
return true;
|
|
120
|
+
return evalFieldCondition(lockedWhen, { ...row });
|
|
121
|
+
}
|
|
113
122
|
/** Options of a select field that are offered for the given row values, in declaration order. */
|
|
114
123
|
export function availableSelectOptions(field, values) {
|
|
115
124
|
const rules = field.conditionalOptions;
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// table growth; irreversible event-log purging is data-retention's job
|
|
12
12
|
// (pruneEvents), a separate, consumer-lag-guarded path.
|
|
13
13
|
import { asEntityTableMeta, deleteMany } from "../db/query.js";
|
|
14
|
+
import { purgeExpiredFileRefsWithBytes } from "../files/purge-expired-file-ref-bytes.js";
|
|
15
|
+
import { FILE_REF_ENTITY_NAME } from "../files/types.js";
|
|
14
16
|
import { SYSTEM_USER_ID } from "./system-user.js";
|
|
15
17
|
// qualifyEntityName convention (feature:type:kebab-name) with a reserved
|
|
16
18
|
// "soft-delete" owner — no real feature owns these; the framework synthesizes
|
|
@@ -77,9 +79,18 @@ export const softDeleteCleanupJob = async (_payload, ctx) => {
|
|
|
77
79
|
if (isSystemScopedSoftDeleteTable(proj.table, entity))
|
|
78
80
|
continue;
|
|
79
81
|
const where = { isDeleted: true, deletedAt: { lt: cutoff }, tenantId };
|
|
80
|
-
await
|
|
82
|
+
await purgeExpiredRows(db, proj.source, proj.table, where, ctx);
|
|
81
83
|
}
|
|
82
84
|
};
|
|
85
|
+
async function purgeExpiredRows(db, source, table, where, ctx) {
|
|
86
|
+
if (source === FILE_REF_ENTITY_NAME) {
|
|
87
|
+
// The row is the only record of the storageKey, so its bytes go first.
|
|
88
|
+
await purgeExpiredFileRefsWithBytes({ db, table, where, files: ctx.files, log: ctx.log });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await deleteMany(db, table, where);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
83
94
|
// System-global soft-deleted entities (tenancy "global" or no tenantId column) can't take a
|
|
84
95
|
// per-tenant grace period — there's no "this tenant's view" of a row that
|
|
85
96
|
// isn't scoped to any tenant. Runs once (not perTenant) with a fixed grace
|
|
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
|
|
|
17
17
|
export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
|
|
18
18
|
export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionRowIdOf, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
|
|
19
19
|
export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
|
|
20
|
-
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
20
|
+
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
21
21
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
22
22
|
export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
|
|
23
23
|
export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
|
package/dist/errors/classes.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export declare class PreconditionFailedError extends ConflictError {
|
|
|
60
60
|
readonly code: string;
|
|
61
61
|
constructor(details: PreconditionFailedDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">);
|
|
62
62
|
}
|
|
63
|
+
export declare class RecordLockedError extends ConflictError {
|
|
64
|
+
readonly code: string;
|
|
65
|
+
constructor(details: {
|
|
66
|
+
readonly entityName: string;
|
|
67
|
+
readonly entityId: number | string;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
63
70
|
export declare class IdempotentReplayError extends ConflictError {
|
|
64
71
|
readonly code: string;
|
|
65
72
|
constructor(details: {
|
package/dist/errors/classes.js
CHANGED
|
@@ -119,6 +119,18 @@ export class PreconditionFailedError extends ConflictError {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
// The entity's `lockedWhen` condition holds for the stored row: no write handler
|
|
123
|
+
// may change or delete it. Same 409 family as the other state conflicts.
|
|
124
|
+
export class RecordLockedError extends ConflictError {
|
|
125
|
+
code = "record_locked";
|
|
126
|
+
constructor(details) {
|
|
127
|
+
super({
|
|
128
|
+
message: `${details.entityName} ${details.entityId} is locked and cannot be changed`,
|
|
129
|
+
i18nKey: "errors.recordLocked",
|
|
130
|
+
details,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
// The caller retried a write with an idempotencyKey it had already used —
|
|
123
135
|
// the event-store's partial unique index on metadata.idempotencyKey caught
|
|
124
136
|
// the duplicate. Distinct from unique_violation/version_conflict: this is
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { FeatureDisabledDetails, FieldIssue, NotFoundDetails, PreconditionFailedDetails, RateLimitDetails, UnconfiguredDetails, UniqueViolationDetails, UnprocessableOpts, ValidationDetails, VersionConflictDetails, } from "./classes.js";
|
|
2
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, RecordLockedError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
3
3
|
export type { ErrorDocsConfig } from "./docs-url.js";
|
|
4
4
|
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
5
5
|
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
1
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, MemberReadTimeoutError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, RecordLockedError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
2
|
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
3
3
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
4
4
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TenantDb } from "../db/tenant-db.js";
|
|
2
|
+
import { type Registry, type SaveContext, type SessionUser } from "../engine/types/index.js";
|
|
3
|
+
export declare function assertFileRefsBelongToTenant(args: {
|
|
4
|
+
readonly registry: Registry;
|
|
5
|
+
readonly result: SaveContext;
|
|
6
|
+
readonly user: SessionUser;
|
|
7
|
+
readonly db: TenantDb;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isFileField, isUuid, } from "../engine/types/index.js";
|
|
2
|
+
import { ValidationError } from "../errors/index.js";
|
|
3
|
+
import { fileRefIdsOf } from "./bind-pending-file-refs.js";
|
|
4
|
+
import { getFileRefExecutor } from "./create-file-ref.js";
|
|
5
|
+
import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
|
|
6
|
+
// A file field only stores an id, and the file routes authorize by tenant at
|
|
7
|
+
// read time: without this check a writer could name another tenant's fileRef
|
|
8
|
+
// and make the record point at it. Only newly written values are checked (all
|
|
9
|
+
// of a create, `changes` of an update), so existing data is left alone. The
|
|
10
|
+
// error is the same for "missing" and "belongs to another tenant" so the id
|
|
11
|
+
// cannot be probed across tenants.
|
|
12
|
+
export async function assertFileRefsBelongToTenant(args) {
|
|
13
|
+
const { registry, result, user, db } = args;
|
|
14
|
+
// skip: saves of unregistered aggregates (custom executors) declare no file fields
|
|
15
|
+
if (result.entityName === undefined)
|
|
16
|
+
return;
|
|
17
|
+
const entity = registry.getEntity(result.entityName);
|
|
18
|
+
// skip: an aggregate without an entity definition has no file fields
|
|
19
|
+
if (entity === undefined)
|
|
20
|
+
return;
|
|
21
|
+
// skip: without the files feature the file_refs table is not part of the schema
|
|
22
|
+
if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const executor = getFileRefExecutor();
|
|
25
|
+
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
26
|
+
if (!isFileField(fieldDef))
|
|
27
|
+
continue;
|
|
28
|
+
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
29
|
+
if (await isFileRefOfTenant(executor, fileRefId, user, db))
|
|
30
|
+
continue;
|
|
31
|
+
throw new ValidationError({
|
|
32
|
+
fields: [
|
|
33
|
+
{
|
|
34
|
+
path: fieldName,
|
|
35
|
+
code: "invalid_element",
|
|
36
|
+
i18nKey: "errors.validation.invalid_element",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function isFileRefOfTenant(executor, fileRefId, user, db) {
|
|
44
|
+
if (!isUuid(fileRefId))
|
|
45
|
+
return false;
|
|
46
|
+
// detail() widens its tenant filter to (self, SYSTEM); only the writer's own
|
|
47
|
+
// tenant counts here, same as GET /files/:id/meta.
|
|
48
|
+
const row = await executor.detail({ id: fileRefId }, user, db);
|
|
49
|
+
return row !== null && row["tenantId"] === user.tenantId;
|
|
50
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TenantDb } from "../db/tenant-db.js";
|
|
2
2
|
import { type Registry, type SaveContext, type SessionUser } from "../engine/types/index.js";
|
|
3
|
+
export declare function fileRefIdsOf(value: unknown): readonly string[];
|
|
3
4
|
export declare function bindPendingFileRefs(args: {
|
|
4
5
|
readonly registry: Registry;
|
|
5
6
|
readonly result: SaveContext;
|
|
@@ -2,7 +2,7 @@ import { isFileField, isUuid, } from "../engine/types/index.js";
|
|
|
2
2
|
import { reraiseAsKumikoError } from "../errors/index.js";
|
|
3
3
|
import { getFileRefExecutor } from "./create-file-ref.js";
|
|
4
4
|
import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
|
|
5
|
-
function fileRefIdsOf(value) {
|
|
5
|
+
export function fileRefIdsOf(value) {
|
|
6
6
|
if (typeof value === "string")
|
|
7
7
|
return isUuid(value) ? [value] : [];
|
|
8
8
|
if (!Array.isArray(value))
|
|
@@ -15,16 +15,17 @@ function isPendingUploadFor(row, entityName, fieldName, user) {
|
|
|
15
15
|
row["fieldName"] === fieldName &&
|
|
16
16
|
row["insertedById"] === user.id);
|
|
17
17
|
}
|
|
18
|
-
// A
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// access work for them. Only the
|
|
22
|
-
//
|
|
23
|
-
// must not pull that file into a
|
|
18
|
+
// A form (or a server handler via createFileRefFromBytes) uploads before the
|
|
19
|
+
// record references the file, so its fileRefs carry entityType + fieldName but
|
|
20
|
+
// no entityId. Binding them in the save's own transaction makes record-bound
|
|
21
|
+
// variants (qr overlay) and record-read file access work for them. Only the
|
|
22
|
+
// caller's own pending uploads for exactly this entity field are bound: naming
|
|
23
|
+
// someone else's upload id in a save payload must not pull that file into a
|
|
24
|
+
// record the caller controls.
|
|
24
25
|
export async function bindPendingFileRefs(args) {
|
|
25
26
|
const { registry, result, user, db } = args;
|
|
26
|
-
// skip:
|
|
27
|
-
if (
|
|
27
|
+
// skip: saves of unnamed aggregates declare no file fields
|
|
28
|
+
if (result.entityName === undefined)
|
|
28
29
|
return;
|
|
29
30
|
const entityName = result.entityName;
|
|
30
31
|
const entity = registry.getEntity(entityName);
|
|
@@ -38,10 +39,9 @@ export async function bindPendingFileRefs(args) {
|
|
|
38
39
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
39
40
|
if (!isFileField(fieldDef))
|
|
40
41
|
continue;
|
|
41
|
-
//
|
|
42
|
-
// the
|
|
43
|
-
const
|
|
44
|
-
for (const fileRefId of fileRefIdsOf(namedIds)) {
|
|
42
|
+
// changes, not data: an update's row carries every stored file id, including
|
|
43
|
+
// ones bound earlier; only the ids this save names can be pending uploads.
|
|
44
|
+
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
45
45
|
const row = await executor.detail({ id: fileRefId }, user, db);
|
|
46
46
|
if (row === null || !isPendingUploadFor(row, entityName, fieldName, user))
|
|
47
47
|
continue;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
export type BindReferencedFilesResult = {
|
|
3
|
+
readonly bound: number;
|
|
4
|
+
readonly skipped: number;
|
|
5
|
+
readonly nextAfterId: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare const bindReferencedFilesWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"bindReferencedFiles", z.ZodObject<{
|
|
8
|
+
entityType: z.ZodString;
|
|
9
|
+
entityIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
|
|
10
|
+
afterId: z.ZodOptional<z.ZodUUID>;
|
|
11
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, z.core.$strip>, {
|
|
13
|
+
bound: number;
|
|
14
|
+
skipped: number;
|
|
15
|
+
nextAfterId: string | null;
|
|
16
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|