@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { qualifyEntityName } from "../qualified-name.js";
|
|
2
2
|
import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
|
|
3
|
-
import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, } from "./screens.js";
|
|
3
|
+
import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, validateDrawerPrefillKeys, } from "./screens.js";
|
|
4
4
|
function isNavigateKind(action) {
|
|
5
5
|
return action.kind === undefined || action.kind === "navigate";
|
|
6
6
|
}
|
|
@@ -59,13 +59,14 @@ function navigateActionsOf(screen) {
|
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
function toNavigateParamsSource(action, params, sourceScreenEntity) {
|
|
62
|
+
function toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel) {
|
|
63
63
|
return {
|
|
64
64
|
...(action.screen !== undefined && { screen: action.screen }),
|
|
65
65
|
...(action.entity !== undefined && { entity: action.entity }),
|
|
66
66
|
...(action.entityId !== undefined && { entityId: action.entityId }),
|
|
67
67
|
params,
|
|
68
68
|
sourceScreenEntity,
|
|
69
|
+
sourceLabel,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
// Every declarative navigate that writes params into the target's URL —
|
|
@@ -73,17 +74,19 @@ function toNavigateParamsSource(action, params, sourceScreenEntity) {
|
|
|
73
74
|
// plus projectionDetail metrics (runMetricNavigate), which reuse that shape —
|
|
74
75
|
// plus a relatedList toolbarAction with no `params` of its own, which still
|
|
75
76
|
// writes its implicit parent-id param at runtime (see `implicitParams`).
|
|
76
|
-
function navigateParamsSources(screen) {
|
|
77
|
+
function navigateParamsSources(screen, sourceLabel) {
|
|
77
78
|
return navigateActionsOf(screen).flatMap(({ action, sourceScreenEntity, implicitParams }) => {
|
|
78
79
|
if (!isNavigateKind(action))
|
|
79
80
|
return [];
|
|
80
81
|
const params = action.params ?? implicitParams;
|
|
81
|
-
return params === undefined
|
|
82
|
+
return params === undefined
|
|
83
|
+
? []
|
|
84
|
+
: [toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel)];
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
|
-
// Per target screen QN,
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
+
// Per target screen QN, every navigate `params` source that writes into the
|
|
88
|
+
// target's URL prefill. A form screen absent from the map accepts no URL prefill.
|
|
89
|
+
function collectUrlPrefillSourcesByScreenQn(features) {
|
|
87
90
|
const screensByShortId = collectScreensByShortId(features);
|
|
88
91
|
const detailForByEntity = new Map();
|
|
89
92
|
for (const feature of features) {
|
|
@@ -95,8 +98,9 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
|
95
98
|
}
|
|
96
99
|
const result = new Map();
|
|
97
100
|
for (const feature of features) {
|
|
98
|
-
for (const screen of Object.
|
|
99
|
-
|
|
101
|
+
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
102
|
+
const sourceLabel = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) navigate action`;
|
|
103
|
+
for (const source of navigateParamsSources(screen, sourceLabel)) {
|
|
100
104
|
const target = resolveTarget(source, screensByShortId, detailForByEntity);
|
|
101
105
|
if (target === undefined)
|
|
102
106
|
continue;
|
|
@@ -105,15 +109,21 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
|
105
109
|
continue;
|
|
106
110
|
}
|
|
107
111
|
const qn = qualifyEntityName(target.featureName, "screen", target.shortId);
|
|
108
|
-
|
|
109
|
-
for (const key of rowFieldExtractorKeys(source.params))
|
|
110
|
-
fields.add(key);
|
|
111
|
-
result.set(qn, fields);
|
|
112
|
+
result.set(qn, [...(result.get(qn) ?? []), source]);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
return result;
|
|
116
117
|
}
|
|
118
|
+
// Per target screen QN, the union of field names any navigate `params` may
|
|
119
|
+
// prefill from the URL.
|
|
120
|
+
export function collectUrlPrefillFieldsByScreenQn(features) {
|
|
121
|
+
const result = new Map();
|
|
122
|
+
for (const [qn, sources] of collectUrlPrefillSourcesByScreenQn(features)) {
|
|
123
|
+
result.set(qn, new Set(sources.flatMap((source) => rowFieldExtractorKeys(source.params))));
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
117
127
|
// Runtime resolution: a bare screen id matches the first feature registering
|
|
118
128
|
// it (create-app's router); an entity target opens its detailFor screen with an id.
|
|
119
129
|
function resolveTarget(source, screensByShortId, detailForByEntity) {
|
|
@@ -124,37 +134,46 @@ function resolveTarget(source, screensByShortId, detailForByEntity) {
|
|
|
124
134
|
const match = screensByShortId.get(source.screen)?.[0];
|
|
125
135
|
return match === undefined ? undefined : { ...match, shortId: source.screen };
|
|
126
136
|
}
|
|
137
|
+
function assertHiddenRequiredFieldsSeeded(featureName, screenId, screen, seeded) {
|
|
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))
|
|
150
|
+
continue;
|
|
151
|
+
throw new Error(`[Feature ${featureName}] 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
|
+
}
|
|
127
159
|
// 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.
|
|
160
|
+
// hides must be seeded by a navigate param — nothing else can ever fill it. The
|
|
161
|
+
// param keys of every source (incl. metric/subtitle navigates and the implicit
|
|
162
|
+
// relatedList parent id) must also be fields the renderer would actually prefill.
|
|
129
163
|
export function validateSubmitPrefilledHiddenFields(features) {
|
|
130
|
-
const
|
|
164
|
+
const sourcesByScreenQn = collectUrlPrefillSourcesByScreenQn(features);
|
|
131
165
|
for (const feature of features) {
|
|
132
166
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
133
167
|
if (screen.type !== "actionForm" && screen.type !== "secretMint")
|
|
134
168
|
continue;
|
|
135
169
|
if (screen.submitPrefilled !== true)
|
|
136
170
|
continue;
|
|
137
|
-
const
|
|
138
|
-
for (const
|
|
139
|
-
|
|
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
|
-
}
|
|
171
|
+
const sources = sourcesByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
|
|
172
|
+
for (const source of sources ?? []) {
|
|
173
|
+
validateDrawerPrefillKeys(source.sourceLabel, "navigate-target", screenId, screen, source.params);
|
|
157
174
|
}
|
|
175
|
+
const seeded = new Set((sources ?? []).flatMap((source) => rowFieldExtractorKeys(source.params)));
|
|
176
|
+
assertHiddenRequiredFieldsSeeded(feature.name, screenId, screen, seeded);
|
|
158
177
|
}
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
+
import { CHANGELOG_TYPES, validateChangelog } from "./feature-changelog.js";
|
|
2
3
|
const BLOCK_RE = /<!--\s*kumiko-changes\s*\n([\s\S]*?)\n\s*-->/g;
|
|
3
|
-
const TYPE_VALUES = new Set(["breaking", "improvement", "fix"]);
|
|
4
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();
|
|
@@ -95,7 +95,7 @@ function parseManualAfterCodemod(fields, source) {
|
|
|
95
95
|
function buildChange(fields, prose, source) {
|
|
96
96
|
const feature = requiredField(fields, "feature", source);
|
|
97
97
|
const rawType = requiredField(fields, "type", source);
|
|
98
|
-
if (!
|
|
98
|
+
if (!isOneOf(CHANGELOG_TYPES, rawType)) {
|
|
99
99
|
throw new Error(`${source}: kumiko-changes type must be breaking, improvement, or fix`);
|
|
100
100
|
}
|
|
101
101
|
const { title, detail } = resolveTitleAndDetail(fields, prose, source);
|
|
@@ -4,9 +4,9 @@ import type { ContainsSecret } from "../secrets/types.js";
|
|
|
4
4
|
import type { QueryHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput } from "./types/define-handler.js";
|
|
5
5
|
import type { KumikoEventTypeMap } from "./types/index.js";
|
|
6
6
|
export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "./types/define-handler.js";
|
|
7
|
-
export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap>(def: WriteHandlerInput<TName, TSchema, TData, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
|
|
7
|
+
export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined>(def: WriteHandlerInput<TName, TSchema, TData, TMap, TPrepared>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
|
|
8
8
|
secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
|
|
9
|
-
] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap>;
|
|
9
|
+
] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap, TPrepared>;
|
|
10
10
|
export declare function defineQueryHandler<const TName extends string, TSchema extends ZodType, TResult = unknown, TMap extends object = KumikoEventTypeMap>(def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TResult> ? [
|
|
11
11
|
secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
|
|
12
12
|
] : []): QueryHandlerDefinition<TName, TSchema, TResult, TMap>;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { runPipeline } from "./run-pipeline.js";
|
|
4
|
+
function pickPreTransactionPhases(def) {
|
|
5
|
+
return {
|
|
6
|
+
...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
|
|
7
|
+
...(def.beforeTransaction && { beforeTransaction: def.beforeTransaction }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
3
10
|
export function defineWriteHandler(def,
|
|
4
11
|
// R6: a phantom rest-param. When the inferred response `TData` carries a
|
|
5
12
|
// Secret<> anywhere, ContainsSecret<TData> is `true` and this resolves to a
|
|
@@ -50,8 +57,9 @@ export function defineWriteHandler(def,
|
|
|
50
57
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
51
58
|
...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
|
|
52
59
|
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
53
|
-
...(def
|
|
60
|
+
...pickPreTransactionPhases(def),
|
|
54
61
|
...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
|
|
62
|
+
...(def.transaction && { transaction: def.transaction }),
|
|
55
63
|
};
|
|
56
64
|
if ("perform" in def && def.perform !== undefined) {
|
|
57
65
|
const performDef = def.perform;
|
|
@@ -88,14 +96,10 @@ export function defineQueryHandler(def,
|
|
|
88
96
|
// brand through into the stored registry entry.
|
|
89
97
|
export const PAGED_QUERY_HANDLER_BRAND = "__kumikoPagedQueryHandler";
|
|
90
98
|
export function isPagedQueryHandler(def) {
|
|
91
|
-
|
|
92
|
-
// otherwise-typed handler definition; the property may legitimately be
|
|
93
|
-
// absent, which is exactly the case this function distinguishes.
|
|
94
|
-
return def[PAGED_QUERY_HANDLER_BRAND] === true;
|
|
99
|
+
return propertyOf(def, PAGED_QUERY_HANDLER_BRAND) === true;
|
|
95
100
|
}
|
|
96
101
|
export function isEntityConventionQueryHandler(def) {
|
|
97
|
-
|
|
98
|
-
return def[ENTITY_CONVENTION_QUERY_BRAND] === true;
|
|
102
|
+
return propertyOf(def, ENTITY_CONVENTION_QUERY_BRAND) === true;
|
|
99
103
|
}
|
|
100
104
|
// A projectionList screen's query must resolve to { rows, nextCursor, total? }
|
|
101
105
|
// (CursorResult<T>) — the renderer used to read rowsQuery.data?.rows and
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
/** Computes a derived cell from its source values. Missing/non-numeric
|
|
2
3
|
* sources: "multiply" with any missing source → undefined; "sum"/"subtract"
|
|
3
4
|
* treat missing as 0 only when at least one source is present — if every
|
|
@@ -6,9 +7,13 @@
|
|
|
6
7
|
* to the target sub-field's declared precision afterward. */
|
|
7
8
|
export function computeDerivedCellValue(op, values) {
|
|
8
9
|
if (op === "multiply") {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let product = 1;
|
|
11
|
+
for (const value of values) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
product *= value;
|
|
15
|
+
}
|
|
16
|
+
return product;
|
|
12
17
|
}
|
|
13
18
|
if (values.every((value) => value === undefined))
|
|
14
19
|
return undefined;
|
|
@@ -58,10 +63,9 @@ function roundHalfAwayFromZero(value, decimals) {
|
|
|
58
63
|
* it's written back, so a fractional product lands on a value the target
|
|
59
64
|
* type can actually represent. */
|
|
60
65
|
export function withDerivedCells(row, derived, schema) {
|
|
61
|
-
if (
|
|
66
|
+
if (!isPlainObject(row))
|
|
62
67
|
return row;
|
|
63
|
-
const
|
|
64
|
-
const copy = { ...source };
|
|
68
|
+
const copy = { ...row };
|
|
65
69
|
for (const [cellName, def] of Object.entries(derived)) {
|
|
66
70
|
const sourceValues = def.from.map((sourceField) => {
|
|
67
71
|
const value = copy[sourceField];
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// validation (feature-ui-extensions.ts's useExtension) already rejects an
|
|
4
4
|
// empty reason, but options stays `Record<string, unknown>`, so orchestrators
|
|
5
5
|
// re-narrow at read time instead of trusting the shape.
|
|
6
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
7
|
export function extensionUsageEscapeHatchReason(usage) {
|
|
7
8
|
const escapeHatch = usage.options?.["escapeHatch"];
|
|
8
|
-
if (
|
|
9
|
+
if (!isPlainObject(escapeHatch))
|
|
9
10
|
return undefined;
|
|
10
11
|
const { reason, grants } = escapeHatch;
|
|
11
12
|
if (Array.isArray(grants) && !grants.includes("unsafeRaw"))
|
package/dist/engine/factories.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
// `personal: { of: "id" }` means the row itself is the subject, not a
|
|
2
3
|
// referenced user — resolves to `recordOwned` instead of `userOwned`.
|
|
3
4
|
const RECORD_OWNER_FIELD = "id";
|
|
@@ -75,15 +76,13 @@ function isNonEmptyString(value) {
|
|
|
75
76
|
// Fail-closed stance gate (kumiko-framework#2921) for the two text-shaped
|
|
76
77
|
// factories — runs before expandPersonalAnnotations so an untyped JS caller
|
|
77
78
|
// (no compiler to stop it) can't slip an unannotated text field past the
|
|
78
|
-
// type layer either.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
79
|
+
// type layer either. The typed signature above already rejects a
|
|
80
|
+
// missing/malformed shape at compile time; this re-checks the same shape at
|
|
81
|
+
// runtime, hence the nullable parameter.
|
|
81
82
|
function assertTextFieldStance(factoryName, overrides) {
|
|
82
83
|
if (overrides === null || typeof overrides !== "object") {
|
|
83
84
|
throw missingTextFieldStanceError(factoryName);
|
|
84
85
|
}
|
|
85
|
-
// @cast-boundary runtime-guard — re-validated field by field below before
|
|
86
|
-
// any value is trusted.
|
|
87
86
|
const { personal, find, reason } = overrides;
|
|
88
87
|
if (personal === undefined || personal === null) {
|
|
89
88
|
throw missingTextFieldStanceError(factoryName);
|
|
@@ -106,7 +105,7 @@ function assertTextFieldStance(factoryName, overrides) {
|
|
|
106
105
|
throw missingTextFieldStanceError(factoryName);
|
|
107
106
|
}
|
|
108
107
|
const validFinds = TEXT_FIELD_STANCE_FIND_VALUES[factoryName];
|
|
109
|
-
if (typeof find !== "string" || !validFinds
|
|
108
|
+
if (typeof find !== "string" || !isOneOf(validFinds, find)) {
|
|
110
109
|
throw missingTextFieldStanceError(factoryName);
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -200,37 +200,45 @@ export function extractNotification(call, sourceFile) {
|
|
|
200
200
|
return fail("notification", sourceLocationFromNode(call, sourceFile), "second argument must be an inline definition object");
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
?.getInitializer()
|
|
207
|
-
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
208
|
-
if (!triggerObj) {
|
|
209
|
-
return fail("notification", sourceLocationFromNode(call, sourceFile), "missing or non-object `trigger` property");
|
|
210
|
-
}
|
|
211
|
-
const onInit = triggerObj
|
|
212
|
-
.getProperty("on")
|
|
213
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
214
|
-
?.getInitializer();
|
|
215
|
-
const onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
216
|
-
if (!onName) {
|
|
217
|
-
return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
|
|
203
|
+
const triggerProp = defObj.getProperty("trigger");
|
|
204
|
+
if (!triggerProp && (defObj.getProperty("recipient") || defObj.getProperty("data"))) {
|
|
205
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient and data require a `trigger`");
|
|
218
206
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
207
|
+
let onName;
|
|
208
|
+
let recipientFn;
|
|
209
|
+
let dataFn;
|
|
210
|
+
if (triggerProp) {
|
|
211
|
+
const triggerObj = triggerProp
|
|
212
|
+
.asKind(SyntaxKind.PropertyAssignment)
|
|
213
|
+
?.getInitializer()
|
|
214
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
215
|
+
if (!triggerObj) {
|
|
216
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "non-object `trigger` property");
|
|
217
|
+
}
|
|
218
|
+
const onInit = triggerObj
|
|
219
|
+
.getProperty("on")
|
|
220
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
221
|
+
?.getInitializer();
|
|
222
|
+
onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
223
|
+
if (!onName) {
|
|
224
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
|
|
225
|
+
}
|
|
226
|
+
const recipientInit = defObj
|
|
227
|
+
.getProperty("recipient")
|
|
228
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
229
|
+
?.getInitializer();
|
|
230
|
+
recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
|
|
231
|
+
if (!recipientFn) {
|
|
232
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient must be an inline arrow function or function expression");
|
|
233
|
+
}
|
|
234
|
+
const dataInit = defObj
|
|
235
|
+
.getProperty("data")
|
|
236
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
237
|
+
?.getInitializer();
|
|
238
|
+
dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
|
|
239
|
+
if (!dataFn) {
|
|
240
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "data must be an inline arrow function or function expression");
|
|
241
|
+
}
|
|
234
242
|
}
|
|
235
243
|
let templates;
|
|
236
244
|
const templatesObj = defObj
|
|
@@ -257,9 +265,11 @@ export function extractNotification(call, sourceFile) {
|
|
|
257
265
|
kind: "notification",
|
|
258
266
|
source: sourceLocationFromNode(call, sourceFile),
|
|
259
267
|
notificationName,
|
|
260
|
-
trigger: { on: onName },
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
...(onName !== undefined && { trigger: { on: onName } }),
|
|
269
|
+
...(recipientFn !== undefined && {
|
|
270
|
+
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
271
|
+
}),
|
|
272
|
+
...(dataFn !== undefined && { dataBody: sourceLocationFromNode(dataFn, sourceFile) }),
|
|
263
273
|
...(templates !== undefined && { templates }),
|
|
264
274
|
});
|
|
265
275
|
}
|
|
@@ -69,11 +69,11 @@ export type AddJobArgs = {
|
|
|
69
69
|
};
|
|
70
70
|
export type AddNotificationArgs = {
|
|
71
71
|
readonly name: string;
|
|
72
|
-
readonly trigger
|
|
72
|
+
readonly trigger?: {
|
|
73
73
|
readonly on: string;
|
|
74
74
|
};
|
|
75
|
-
readonly recipientSource
|
|
76
|
-
readonly dataSource
|
|
75
|
+
readonly recipientSource?: string;
|
|
76
|
+
readonly dataSource?: string;
|
|
77
77
|
readonly templates?: Readonly<Record<string, string>>;
|
|
78
78
|
};
|
|
79
79
|
export type AddHttpRouteArgs = {
|
|
@@ -205,9 +205,9 @@ export function createFeaturePatcher(sourceFile) {
|
|
|
205
205
|
kind: "notification",
|
|
206
206
|
source: SYNTHETIC_LOC,
|
|
207
207
|
notificationName: name,
|
|
208
|
-
trigger,
|
|
209
|
-
recipientBody: rawLoc(recipientSource),
|
|
210
|
-
dataBody: rawLoc(dataSource),
|
|
208
|
+
...(trigger !== undefined && { trigger }),
|
|
209
|
+
...(recipientSource !== undefined && { recipientBody: rawLoc(recipientSource) }),
|
|
210
|
+
...(dataSource !== undefined && { dataBody: rawLoc(dataSource) }),
|
|
211
211
|
...(templateLocs !== undefined && { templates: templateLocs }),
|
|
212
212
|
});
|
|
213
213
|
},
|
|
@@ -202,6 +202,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
202
202
|
limit: z.ZodNumber;
|
|
203
203
|
windowSeconds: z.ZodNumber;
|
|
204
204
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
205
|
+
normalize: z.ZodOptional<z.ZodLiteral<"email">>;
|
|
205
206
|
}, z.core.$strict>>, z.ZodObject<{
|
|
206
207
|
__raw: z.ZodString;
|
|
207
208
|
}, z.core.$strict>]>>;
|
|
@@ -366,11 +367,11 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
366
367
|
kind: z.ZodLiteral<"notification">;
|
|
367
368
|
source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
|
|
368
369
|
notificationName: z.ZodString;
|
|
369
|
-
trigger: z.ZodObject<{
|
|
370
|
+
trigger: z.ZodOptional<z.ZodObject<{
|
|
370
371
|
on: z.ZodString;
|
|
371
|
-
}, z.core.$strict
|
|
372
|
-
recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
373
|
-
dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
372
|
+
}, z.core.$strict>>;
|
|
373
|
+
recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
374
|
+
dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
374
375
|
templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
|
|
375
376
|
}, z.core.$strict>;
|
|
376
377
|
authClaims: z.ZodObject<{
|
|
@@ -724,6 +725,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
724
725
|
limit: z.ZodNumber;
|
|
725
726
|
windowSeconds: z.ZodNumber;
|
|
726
727
|
cost: z.ZodOptional<z.ZodNumber>;
|
|
728
|
+
normalize: z.ZodOptional<z.ZodLiteral<"email">>;
|
|
727
729
|
}, z.core.$strict>>, z.ZodObject<{
|
|
728
730
|
__raw: z.ZodString;
|
|
729
731
|
}, z.core.$strict>]>>;
|
|
@@ -883,11 +885,11 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
883
885
|
kind: z.ZodLiteral<"notification">;
|
|
884
886
|
source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
|
|
885
887
|
notificationName: z.ZodString;
|
|
886
|
-
trigger: z.ZodObject<{
|
|
888
|
+
trigger: z.ZodOptional<z.ZodObject<{
|
|
887
889
|
on: z.ZodString;
|
|
888
|
-
}, z.core.$strict
|
|
889
|
-
recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
890
|
-
dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
890
|
+
}, z.core.$strict>>;
|
|
891
|
+
recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
892
|
+
dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
891
893
|
templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
|
|
892
894
|
}, z.core.$strict>, z.ZodObject<{
|
|
893
895
|
kind: z.ZodLiteral<"authClaims">;
|
|
@@ -163,6 +163,7 @@ const additionalRateLimitsSchema = z.array(z
|
|
|
163
163
|
limit: z.number(),
|
|
164
164
|
windowSeconds: z.number(),
|
|
165
165
|
cost: z.number().optional(),
|
|
166
|
+
normalize: z.literal("email").optional(),
|
|
166
167
|
})
|
|
167
168
|
.strict());
|
|
168
169
|
const agentHandlerHintsSchema = z
|
|
@@ -562,9 +563,9 @@ const notificationSchema = z
|
|
|
562
563
|
kind: z.literal("notification"),
|
|
563
564
|
source: sourceFieldSchema,
|
|
564
565
|
notificationName: z.string(),
|
|
565
|
-
trigger: z.object({ on: z.string() }).strict(),
|
|
566
|
-
recipientBody: sourceBodySchema,
|
|
567
|
-
dataBody: sourceBodySchema,
|
|
566
|
+
trigger: z.object({ on: z.string() }).strict().optional(),
|
|
567
|
+
recipientBody: sourceBodySchema.optional(),
|
|
568
|
+
dataBody: sourceBodySchema.optional(),
|
|
568
569
|
templates: z.record(z.string(), sourceBodySchema).optional(),
|
|
569
570
|
})
|
|
570
571
|
.strict();
|
|
@@ -190,11 +190,11 @@ export type NotificationPattern = {
|
|
|
190
190
|
readonly kind: "notification";
|
|
191
191
|
readonly source: SourceLocation;
|
|
192
192
|
readonly notificationName: string;
|
|
193
|
-
readonly trigger
|
|
193
|
+
readonly trigger?: {
|
|
194
194
|
readonly on: string;
|
|
195
195
|
};
|
|
196
|
-
readonly recipientBody
|
|
197
|
-
readonly dataBody
|
|
196
|
+
readonly recipientBody?: SourceLocation;
|
|
197
|
+
readonly dataBody?: SourceLocation;
|
|
198
198
|
readonly templates?: Readonly<Record<string, SourceLocation>>;
|
|
199
199
|
};
|
|
200
200
|
export type AuthClaimsPattern = {
|
|
@@ -240,18 +240,26 @@ function renderMetric(p) {
|
|
|
240
240
|
const merged = { name: p.shortName, ...p.options };
|
|
241
241
|
return `r.metric(${renderValue(merged)});`;
|
|
242
242
|
}
|
|
243
|
+
// r.secret / r.secretNamespace throw on an empty writeRoles list; the designer's
|
|
244
|
+
// string-list input emits [] when cleared, which means "rely on handler access alone".
|
|
245
|
+
function dropEmptyWriteRoles(options) {
|
|
246
|
+
if (options.writeRoles?.length !== 0)
|
|
247
|
+
return options;
|
|
248
|
+
const { writeRoles: _emptyWriteRoles, ...rest } = options;
|
|
249
|
+
return rest;
|
|
250
|
+
}
|
|
243
251
|
function renderSecret(p) {
|
|
244
252
|
if (isRawRefSentinel(p.options)) {
|
|
245
253
|
return `r.secret(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
246
254
|
}
|
|
247
|
-
const merged = { name: p.shortName, ...p.options };
|
|
255
|
+
const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
|
|
248
256
|
return `r.secret(${renderValue(merged)});`;
|
|
249
257
|
}
|
|
250
258
|
function renderSecretNamespace(p) {
|
|
251
259
|
if (isRawRefSentinel(p.options)) {
|
|
252
260
|
return `r.secretNamespace(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
|
|
253
261
|
}
|
|
254
|
-
const merged = { name: p.shortName, ...p.options };
|
|
262
|
+
const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
|
|
255
263
|
return `r.secretNamespace(${renderValue(merged)});`;
|
|
256
264
|
}
|
|
257
265
|
function renderClaimKey(p) {
|
|
@@ -497,9 +505,12 @@ function renderJob(p) {
|
|
|
497
505
|
function renderNotification(p) {
|
|
498
506
|
const lines = ["r.notification({"];
|
|
499
507
|
lines.push(` name: ${JSON.stringify(p.notificationName)},`);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
508
|
+
if (p.trigger)
|
|
509
|
+
lines.push(` trigger: { on: ${JSON.stringify(p.trigger.on)} },`);
|
|
510
|
+
if (p.recipientBody)
|
|
511
|
+
lines.push(` recipient: ${p.recipientBody.raw},`);
|
|
512
|
+
if (p.dataBody)
|
|
513
|
+
lines.push(` data: ${p.dataBody.raw},`);
|
|
503
514
|
if (p.templates && Object.keys(p.templates).length > 0) {
|
|
504
515
|
lines.push(" templates: {");
|
|
505
516
|
for (const [k, loc] of Object.entries(p.templates)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const CHANGELOG_TYPES: readonly ["breaking", "improvement", "fix"];
|
|
2
|
+
export type ChangelogType = (typeof CHANGELOG_TYPES)[number];
|
|
2
3
|
export type ChangelogEntry = {
|
|
3
4
|
readonly version: string;
|
|
4
5
|
readonly type: ChangelogType;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
// File I/O stays in the CLI (`bin/commands/upgrade.ts`) — this module is
|
|
7
7
|
// pure parse/validate so engine stays off the node:fs allowlist.
|
|
8
|
+
import { isOneOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
9
|
+
export const CHANGELOG_TYPES = ["breaking", "improvement", "fix"];
|
|
8
10
|
/** Parse a changes.json body. Callers own file I/O. */
|
|
9
11
|
export function parseFeatureChangelog(raw, featureName) {
|
|
10
12
|
try {
|
|
@@ -24,16 +26,16 @@ export function parseFeatureChangelog(raw, featureName) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
function isChangelogEntry(value) {
|
|
27
|
-
if (
|
|
29
|
+
if (!isPlainObject(value))
|
|
28
30
|
return false;
|
|
29
|
-
|
|
30
|
-
if (typeof obj["version"] !== "string")
|
|
31
|
+
if (typeof value["version"] !== "string")
|
|
31
32
|
return false;
|
|
32
|
-
if (!
|
|
33
|
+
if (!isOneOf(CHANGELOG_TYPES, value["type"]))
|
|
33
34
|
return false;
|
|
34
|
-
if (typeof
|
|
35
|
+
if (typeof value["title"] !== "string")
|
|
35
36
|
return false;
|
|
36
|
-
if (
|
|
37
|
+
if (value["manualAfterCodemod"] !== undefined &&
|
|
38
|
+
typeof value["manualAfterCodemod"] !== "boolean") {
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
41
|
return true;
|