@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
|
@@ -288,7 +288,8 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
288
288
|
// Multi file: array of fileRef UUIDs. Same story as the singular
|
|
289
289
|
// variant — the element type has to match the UUID column on
|
|
290
290
|
// fileRefsTable.id.
|
|
291
|
-
|
|
291
|
+
const ids = z.array(z.uuid());
|
|
292
|
+
return field.maxCount === undefined ? ids : ids.max(field.maxCount);
|
|
292
293
|
}
|
|
293
294
|
case "reference":
|
|
294
295
|
// Tier 2.7e-3: Validiert UUID-shape. Existenz-Check der Reference
|
|
@@ -358,9 +359,7 @@ function applyTotalsMatchRefinements(entity, schema) {
|
|
|
358
359
|
continue;
|
|
359
360
|
}
|
|
360
361
|
const sumMinor = rawRows.reduce((total, row) => {
|
|
361
|
-
const value = typeof row === "object" && row !== null
|
|
362
|
-
? row[subFieldName]
|
|
363
|
-
: undefined;
|
|
362
|
+
const value = typeof row === "object" && row !== null ? Reflect.get(row, subFieldName) : undefined;
|
|
364
363
|
return total + (typeof value === "number" ? value : 0);
|
|
365
364
|
}, 0);
|
|
366
365
|
if (sumMinor !== siblingMinor) {
|
|
@@ -11,6 +11,7 @@
|
|
|
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 { hasFieldProperty } from "../db/table-introspection.js";
|
|
14
15
|
import { purgeExpiredFileRefsWithBytes } from "../files/purge-expired-file-ref-bytes.js";
|
|
15
16
|
import { FILE_REF_ENTITY_NAME } from "../files/types.js";
|
|
16
17
|
import { SYSTEM_USER_ID } from "./system-user.js";
|
|
@@ -34,7 +35,7 @@ export const softDeleteGraceDaysConfig = {
|
|
|
34
35
|
bounds: { min: 1 },
|
|
35
36
|
};
|
|
36
37
|
function hasTenantIdColumn(table) {
|
|
37
|
-
return table
|
|
38
|
+
return hasFieldProperty(table, "tenantId");
|
|
38
39
|
}
|
|
39
40
|
// The entity declaration decides, not the column: a `tenancy: "global"` table
|
|
40
41
|
// (e.g. user) still carries a tenantId column (SYSTEM_TENANT_ID), yet no
|
|
@@ -79,13 +80,20 @@ export const softDeleteCleanupJob = async (_payload, ctx) => {
|
|
|
79
80
|
if (isSystemScopedSoftDeleteTable(proj.table, entity))
|
|
80
81
|
continue;
|
|
81
82
|
const where = { isDeleted: true, deletedAt: { lt: cutoff }, tenantId };
|
|
82
|
-
await purgeExpiredRows(db, proj.source, proj.table, where, ctx);
|
|
83
|
+
await purgeExpiredRows(db, proj.source, proj.table, where, cutoff, ctx);
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
|
-
async function purgeExpiredRows(db, source, table, where, ctx) {
|
|
86
|
+
async function purgeExpiredRows(db, source, table, where, cutoff, ctx) {
|
|
86
87
|
if (source === FILE_REF_ENTITY_NAME) {
|
|
87
88
|
// The row is the only record of the storageKey, so its bytes go first.
|
|
88
|
-
await purgeExpiredFileRefsWithBytes({
|
|
89
|
+
await purgeExpiredFileRefsWithBytes({
|
|
90
|
+
db,
|
|
91
|
+
table,
|
|
92
|
+
where,
|
|
93
|
+
expiredBefore: cutoff,
|
|
94
|
+
files: ctx.files,
|
|
95
|
+
log: ctx.log,
|
|
96
|
+
});
|
|
89
97
|
}
|
|
90
98
|
else {
|
|
91
99
|
await deleteMany(db, table, where);
|
|
@@ -18,9 +18,7 @@ defineStep({
|
|
|
18
18
|
// dispatcher's catch maps it back to a typed error response
|
|
19
19
|
// (e.g. validation_failed stays validation_failed, not a
|
|
20
20
|
// generic internal_error).
|
|
21
|
-
|
|
22
|
-
err.cause = result.error;
|
|
23
|
-
throw err;
|
|
21
|
+
throw new Error(`callFeature("${args.handler}") returned failure`, { cause: result.error });
|
|
24
22
|
}
|
|
25
23
|
return result.data;
|
|
26
24
|
},
|
|
@@ -11,6 +11,7 @@ import { defineStep } from "../define-step.js";
|
|
|
11
11
|
import { runStepList } from "../run-pipeline.js";
|
|
12
12
|
import { SUSPEND_SENTINEL, WORKFLOW_AGGREGATE_TYPE, WORKFLOW_RETRY_SCHEDULED_TYPE, } from "./_step-dispatch-constants.js";
|
|
13
13
|
import { describeWorkflowStepError } from "./describe-workflow-step-error.js";
|
|
14
|
+
import { unwrapWorkflowStepError } from "./workflow-step-error.js";
|
|
14
15
|
const log = createFallbackLogger("workflow-retry");
|
|
15
16
|
defineStep({
|
|
16
17
|
kind: "workflow.retry",
|
|
@@ -38,7 +39,7 @@ defineStep({
|
|
|
38
39
|
workflowName: ctx.workflow.workflowName,
|
|
39
40
|
stepIndex: ctx.workflow.stepIndex,
|
|
40
41
|
attempt,
|
|
41
|
-
error: String(error),
|
|
42
|
+
error: String(unwrapWorkflowStepError(error)),
|
|
42
43
|
});
|
|
43
44
|
const backoffMs = calculateBackoff(attempt, args.backoff);
|
|
44
45
|
const wakeAt = Temporal.Now.instant().add({ milliseconds: backoffMs }).toString();
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// must commit in the same TX as the aggregate-mutation that triggered
|
|
17
17
|
// it (stronger consistency than an async projection). Reviewer judges.
|
|
18
18
|
import { deleteMany } from "../../db/query.js";
|
|
19
|
-
import {
|
|
19
|
+
import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
|
|
20
20
|
import { defineStep } from "../define-step.js";
|
|
21
21
|
import { resolveRequired } from "./_resolver-utils.js";
|
|
22
22
|
defineStep({
|
|
@@ -27,7 +27,7 @@ defineStep({
|
|
|
27
27
|
// The `unsafe*`-step IS the sanctioned direct projection write — it exists
|
|
28
28
|
// to bypass the executor deliberately (boot-validated allowlist + reviewer
|
|
29
29
|
// gate). The brand-strip cast makes that explicit at the one blessed seam.
|
|
30
|
-
await deleteMany(
|
|
30
|
+
await deleteMany(tenantDbWriteRunner(ctx.db, "unsafe projection delete"), args.table, where);
|
|
31
31
|
},
|
|
32
32
|
});
|
|
33
33
|
export function buildUnsafeProjectionDeleteStep(args) {
|
|
@@ -10,25 +10,14 @@
|
|
|
10
10
|
// the owning feature. Aggregate-tables (registered via r.entity) are
|
|
11
11
|
// rejected by boot-validation — domain mutation MUST go through
|
|
12
12
|
// r.step.aggregate.*.
|
|
13
|
-
import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
14
13
|
import { extractTableName } from "../../db/index.js";
|
|
15
14
|
import { executeRawQuery } from "../../db/queries/raw-sql.js";
|
|
16
|
-
import {
|
|
15
|
+
import { columnSqlNameOf } from "../../db/table-introspection.js";
|
|
16
|
+
import { tenantDbWriteRunner } from "../../db/tenant-db-runner.js";
|
|
17
17
|
import { defineStep } from "../define-step.js";
|
|
18
18
|
import { resolveRequired } from "./_resolver-utils.js";
|
|
19
19
|
function resolveColumnName(table, field) {
|
|
20
|
-
|
|
21
|
-
return field;
|
|
22
|
-
const cols = table[KUMIKO_COLUMNS_SYMBOL];
|
|
23
|
-
if (typeof cols !== "object" || cols === null)
|
|
24
|
-
return field;
|
|
25
|
-
const col = cols[field];
|
|
26
|
-
if (typeof col === "object" && col !== null) {
|
|
27
|
-
const nameVal = col["name"];
|
|
28
|
-
if (typeof nameVal === "string")
|
|
29
|
-
return nameVal;
|
|
30
|
-
}
|
|
31
|
-
return field;
|
|
20
|
+
return columnSqlNameOf(table, field) ?? field;
|
|
32
21
|
}
|
|
33
22
|
function quoteIdent(name) {
|
|
34
23
|
return `"${name.replace(/"/g, '""')}"`;
|
|
@@ -65,7 +54,7 @@ defineStep({
|
|
|
65
54
|
}
|
|
66
55
|
const sqlText = `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
|
|
67
56
|
`ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
|
|
68
|
-
await executeRawQuery(
|
|
57
|
+
await executeRawQuery(tenantDbWriteRunner(ctx.db, "unsafe projection upsert"), sqlText, params);
|
|
69
58
|
},
|
|
70
59
|
});
|
|
71
60
|
export function buildUnsafeProjectionUpsertStep(args) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { Registry, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariables } from "./types/index.js";
|
|
3
|
+
type CommonOptions = {
|
|
4
|
+
readonly label?: string;
|
|
5
|
+
readonly required?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare const templateVariable: {
|
|
8
|
+
readonly text: (opts?: CommonOptions & {
|
|
9
|
+
readonly example?: string;
|
|
10
|
+
}) => TemplateScalarVariableDef;
|
|
11
|
+
readonly link: (opts?: CommonOptions & {
|
|
12
|
+
readonly example?: string;
|
|
13
|
+
}) => TemplateScalarVariableDef;
|
|
14
|
+
readonly richText: (opts?: CommonOptions & {
|
|
15
|
+
readonly example?: string;
|
|
16
|
+
}) => TemplateScalarVariableDef;
|
|
17
|
+
readonly money: (opts?: CommonOptions & {
|
|
18
|
+
readonly example?: number | {
|
|
19
|
+
readonly amount: number;
|
|
20
|
+
readonly currency: string;
|
|
21
|
+
};
|
|
22
|
+
}) => TemplateScalarVariableDef;
|
|
23
|
+
readonly date: (opts?: CommonOptions & {
|
|
24
|
+
readonly example?: string;
|
|
25
|
+
}) => TemplateScalarVariableDef;
|
|
26
|
+
readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
|
|
27
|
+
readonly example?: readonly Readonly<Record<string, unknown>>[];
|
|
28
|
+
}) => TemplateListVariableDef;
|
|
29
|
+
};
|
|
30
|
+
export declare function templateVariablesToZod(variables: TemplateVariables): z.ZodObject;
|
|
31
|
+
export declare function assertNotificationVariables(registry: Pick<Registry, "getAllNotifications">, notificationType: string, data: Readonly<Record<string, unknown>> | undefined): void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { InternalError } from "../errors/index.js";
|
|
3
|
+
// Builders keep declarations terse and the `type` discriminant out of app code.
|
|
4
|
+
export const templateVariable = {
|
|
5
|
+
text: (opts = {}) => ({
|
|
6
|
+
type: "text",
|
|
7
|
+
...opts,
|
|
8
|
+
}),
|
|
9
|
+
link: (opts = {}) => ({
|
|
10
|
+
type: "link",
|
|
11
|
+
...opts,
|
|
12
|
+
}),
|
|
13
|
+
richText: (opts = {}) => ({ type: "richText", ...opts }),
|
|
14
|
+
money: (opts = {}) => ({ type: "money", ...opts }),
|
|
15
|
+
date: (opts = {}) => ({
|
|
16
|
+
type: "date",
|
|
17
|
+
...opts,
|
|
18
|
+
}),
|
|
19
|
+
list: (item, opts = {}) => ({ type: "list", item, ...opts }),
|
|
20
|
+
};
|
|
21
|
+
const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
22
|
+
function isSafeLinkUrl(value) {
|
|
23
|
+
try {
|
|
24
|
+
return LINK_PROTOCOLS.has(new URL(value).protocol);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const linkSchema = z.string().refine(isSafeLinkUrl, "must be an http, https or mailto URL");
|
|
31
|
+
const moneySchema = z.union([
|
|
32
|
+
z.number().int(),
|
|
33
|
+
z.object({
|
|
34
|
+
amount: z.number().int(),
|
|
35
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
const dateSchema = z.union([z.iso.date(), z.iso.datetime({ offset: true })]);
|
|
39
|
+
function scalarToZod(def) {
|
|
40
|
+
switch (def.type) {
|
|
41
|
+
case "text":
|
|
42
|
+
return z.string();
|
|
43
|
+
case "richText":
|
|
44
|
+
return z.string();
|
|
45
|
+
case "link":
|
|
46
|
+
return linkSchema;
|
|
47
|
+
case "money":
|
|
48
|
+
return moneySchema;
|
|
49
|
+
case "date":
|
|
50
|
+
return dateSchema;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function variableToZod(def) {
|
|
54
|
+
if (def.type !== "list")
|
|
55
|
+
return scalarToZod(def);
|
|
56
|
+
return z.array(z.object(Object.fromEntries(Object.entries(def.item).map(([name, item]) => [name, withOptionality(item)]))));
|
|
57
|
+
}
|
|
58
|
+
function withOptionality(def) {
|
|
59
|
+
const schema = variableToZod(def);
|
|
60
|
+
return def.required === true ? schema : schema.optional();
|
|
61
|
+
}
|
|
62
|
+
export function templateVariablesToZod(variables) {
|
|
63
|
+
return z.object(Object.fromEntries(Object.entries(variables).map(([name, def]) => [name, withOptionality(def)])));
|
|
64
|
+
}
|
|
65
|
+
// Validates only: the caller keeps passing the original data on, because
|
|
66
|
+
// parsing would strip keys the delivery adds itself (unsubscribeUrl, fromName).
|
|
67
|
+
export function assertNotificationVariables(registry, notificationType, data) {
|
|
68
|
+
const definition = registry.getAllNotifications().get(notificationType);
|
|
69
|
+
if (definition?.template !== undefined && definition.variables !== undefined) {
|
|
70
|
+
const parsed = templateVariablesToZod(definition.variables).safeParse(data ?? {});
|
|
71
|
+
if (!parsed.success) {
|
|
72
|
+
const issues = parsed.error.issues
|
|
73
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
74
|
+
.join("; ");
|
|
75
|
+
throw new InternalError({
|
|
76
|
+
message: `notification "${notificationType}" has invalid variables: ${issues}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// **Apps ohne tier-engine:** wenn keine plugin registriert ist, framework
|
|
21
21
|
// macht nichts — `effectiveFeatures` bleibt undefined, alle features sind on.
|
|
22
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
22
23
|
/**
|
|
23
24
|
* Extension-name unter dem ein tier-resolver-plugin im registry registriert
|
|
24
25
|
* werden muss. Konstante damit `r.useExtension(TENANT_TIER_RESOLVER_EXT, ...)`
|
|
@@ -39,9 +40,7 @@ export const TENANT_TIER_RESOLVER_EXT = "tenantTierResolver";
|
|
|
39
40
|
* plugin shape — the only thing every implementation must provide is the
|
|
40
41
|
* `build` factory. */
|
|
41
42
|
export function isTierResolverPlugin(v) {
|
|
42
|
-
return (typeof v === "
|
|
43
|
-
v !== null &&
|
|
44
|
-
typeof v["build"] === "function");
|
|
43
|
+
return isPlainObject(v) && typeof v["build"] === "function";
|
|
45
44
|
}
|
|
46
45
|
export function findTierResolverUsage(features) {
|
|
47
46
|
for (const feature of features) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
2
|
-
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
2
|
+
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
3
3
|
export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
|
|
4
4
|
export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
|
|
5
5
|
export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
|
|
@@ -7,7 +7,7 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
|
|
|
7
7
|
export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
8
8
|
export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
|
|
9
9
|
export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
|
|
10
|
-
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
10
|
+
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
11
11
|
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
12
12
|
export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
|
|
13
13
|
export type { HttpRouteDefinition, HttpRouteHandler, HttpRouteHandlerDeps, HttpRouteMethod, } from "@cosmicdrift/kumiko-types/http-route";
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
// boot-time is surfaced cleanly; a quietly-different step-list is not
|
|
23
23
|
// caught by this validator. A future lint-rule will enforce the contract
|
|
24
24
|
// statically; today it lives in this comment + the StepBuilder doc.
|
|
25
|
-
import { KUMIKO_META_SYMBOL
|
|
25
|
+
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
26
|
+
import { schemaTableName } from "../db/table-introspection.js";
|
|
26
27
|
import { getStep } from "./define-step.js";
|
|
27
28
|
import { buildPipelineSteps } from "./pipeline.js";
|
|
28
29
|
// Listed step-kinds whose `args.table` must be in the owning feature's
|
|
@@ -57,21 +58,19 @@ function* walkAllSteps(steps) {
|
|
|
57
58
|
function resolveTableNameFromStep(table) {
|
|
58
59
|
if (typeof table === "object" && table !== null) {
|
|
59
60
|
// Canonical meta under the unshadowable symbol — preferred path.
|
|
60
|
-
const meta = table
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const meta = Reflect.get(table, KUMIKO_META_SYMBOL);
|
|
62
|
+
if (typeof meta === "object" &&
|
|
63
|
+
meta !== null &&
|
|
64
|
+
"tableName" in meta &&
|
|
65
|
+
typeof meta.tableName === "string") {
|
|
66
|
+
return meta.tableName;
|
|
65
67
|
}
|
|
66
68
|
// Plain meta (deriveEntityTableMeta / defineUnmanagedTable — no handle-spread).
|
|
67
|
-
if ("source" in table &&
|
|
68
|
-
|
|
69
|
-
typeof table["tableName"] === "string") {
|
|
70
|
-
return table["tableName"];
|
|
69
|
+
if ("source" in table && "tableName" in table && typeof table.tableName === "string") {
|
|
70
|
+
return table.tableName;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (typeof name === "string")
|
|
72
|
+
const name = schemaTableName(table);
|
|
73
|
+
if (name !== undefined)
|
|
75
74
|
return name;
|
|
76
75
|
}
|
|
77
76
|
throw new Error(`validate-projection-allowlist: cannot resolve table name from ${String(table)}`);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// parser) that catches the two shapes that always mean "this rule is
|
|
9
9
|
// broken": a bare column reference inside a subquery that shadows an outer
|
|
10
10
|
// column, and a literal self-comparison.
|
|
11
|
-
import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
12
11
|
import { toSnakeCase } from "../db/table-builder.js";
|
|
12
|
+
import { tableColumnsOf } from "../db/table-introspection.js";
|
|
13
13
|
const IDENT_SRC = `"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`;
|
|
14
14
|
const QUALIFIED_SRC = `(?:${IDENT_SRC})(?:\\.(?:${IDENT_SRC}))?`;
|
|
15
15
|
const TOKEN_RE = new RegExp(IDENT_SRC, "g");
|
|
@@ -23,14 +23,15 @@ const QUALIFIED_PARSE_RE = new RegExp(`^(${IDENT_SRC})(?:\\.(${IDENT_SRC}))?$`);
|
|
|
23
23
|
export function tableColumnSqlNames(table) {
|
|
24
24
|
if (table === null || typeof table !== "object")
|
|
25
25
|
return new Set();
|
|
26
|
-
const cols = table
|
|
27
|
-
if (cols
|
|
26
|
+
const cols = tableColumnsOf(table);
|
|
27
|
+
if (cols) {
|
|
28
28
|
const names = new Set();
|
|
29
29
|
for (const col of Object.values(cols)) {
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (typeof col === "object" &&
|
|
31
|
+
col !== null &&
|
|
32
|
+
"name" in col &&
|
|
33
|
+
typeof col.name === "string") {
|
|
34
|
+
names.add(col.name.toLowerCase());
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
return names;
|
package/dist/errors/classes.js
CHANGED
|
@@ -223,6 +223,7 @@ export class RateLimitError extends KumikoError {
|
|
|
223
223
|
message: `rate limited: ${details.bucket} (limit ${details.limit} per ${details.windowSeconds}s)`,
|
|
224
224
|
i18nKey: opts?.i18nKey ?? "errors.rate_limited",
|
|
225
225
|
details,
|
|
226
|
+
retryAfterSeconds: details.retryAfterSeconds,
|
|
226
227
|
...(opts?.cause && { cause: opts.cause }),
|
|
227
228
|
});
|
|
228
229
|
this.details = details;
|
|
@@ -11,6 +11,7 @@ export type ErrorCtorInput = {
|
|
|
11
11
|
readonly i18nParams?: Readonly<Record<string, unknown>>;
|
|
12
12
|
readonly details?: unknown;
|
|
13
13
|
readonly cause?: Error;
|
|
14
|
+
readonly retryAfterSeconds?: number;
|
|
14
15
|
};
|
|
15
16
|
export declare abstract class KumikoError extends Error {
|
|
16
17
|
abstract readonly code: string;
|
|
@@ -18,6 +19,7 @@ export declare abstract class KumikoError extends Error {
|
|
|
18
19
|
readonly i18nKey: string;
|
|
19
20
|
readonly i18nParams: Readonly<Record<string, unknown>> | undefined;
|
|
20
21
|
readonly details: unknown;
|
|
22
|
+
readonly retryAfterSeconds: number | undefined;
|
|
21
23
|
constructor(input: ErrorCtorInput);
|
|
22
24
|
}
|
|
23
25
|
export declare function isKumikoError(e: unknown): e is KumikoError;
|
|
@@ -6,12 +6,14 @@ export class KumikoError extends Error {
|
|
|
6
6
|
i18nKey;
|
|
7
7
|
i18nParams;
|
|
8
8
|
details;
|
|
9
|
+
retryAfterSeconds;
|
|
9
10
|
constructor(input) {
|
|
10
11
|
super(input.message, input.cause ? { cause: input.cause } : undefined);
|
|
11
12
|
this.name = new.target.name;
|
|
12
13
|
this.i18nKey = input.i18nKey;
|
|
13
14
|
this.i18nParams = input.i18nParams;
|
|
14
15
|
this.details = input.details;
|
|
16
|
+
this.retryAfterSeconds = input.retryAfterSeconds;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
export function isKumikoError(e) {
|
package/dist/errors/reasons.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const FrameworkReasons: {
|
|
|
7
7
|
readonly systemIdentitySwitchDenied: "system_identity_switch_denied";
|
|
8
8
|
readonly identitySwitchDenied: "identity_switch_denied";
|
|
9
9
|
readonly tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin";
|
|
10
|
+
readonly notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast";
|
|
10
11
|
readonly unsafeAllTenantsDenied: "unsafe_all_tenants_denied";
|
|
11
12
|
readonly memberResolutionDenied: "member_resolution_denied";
|
|
12
13
|
readonly memberResolutionReadOnly: "member_resolution_read_only";
|
package/dist/errors/reasons.js
CHANGED
|
@@ -42,6 +42,8 @@ export const FrameworkReasons = {
|
|
|
42
42
|
identitySwitchDenied: "identity_switch_denied",
|
|
43
43
|
// AccessDeniedError: a payload carried tenantIdOverride but the caller is no SystemAdmin.
|
|
44
44
|
tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin",
|
|
45
|
+
// AccessDeniedError: ctx.notify was called with `to: { tenant }` naming a tenant other than the caller's.
|
|
46
|
+
notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast",
|
|
45
47
|
// AccessDeniedError: a handler/hook called ctx.queryProjection(..., { unsafeAllTenants: true })
|
|
46
48
|
// without an r.systemScope() feature or a declared escapeHatch.
|
|
47
49
|
unsafeAllTenantsDenied: "unsafe_all_tenants_denied",
|
|
@@ -69,6 +69,7 @@ export function toWriteErrorInfo(err) {
|
|
|
69
69
|
message: err.message,
|
|
70
70
|
...(err.i18nParams && { i18nParams: err.i18nParams }),
|
|
71
71
|
...(effectiveDetails !== undefined && { details: effectiveDetails }),
|
|
72
|
+
...(err.retryAfterSeconds !== undefined && { retryAfterSeconds: err.retryAfterSeconds }),
|
|
72
73
|
};
|
|
73
74
|
if (err.cause instanceof Error)
|
|
74
75
|
causeByWriteErrorInfo.set(info, err.cause);
|
|
@@ -91,6 +92,7 @@ class ReraisedError extends KumikoError {
|
|
|
91
92
|
i18nKey: info.i18nKey,
|
|
92
93
|
...(info.i18nParams && { i18nParams: info.i18nParams }),
|
|
93
94
|
...(info.details !== undefined && { details: info.details }),
|
|
95
|
+
...(info.retryAfterSeconds !== undefined && { retryAfterSeconds: info.retryAfterSeconds }),
|
|
94
96
|
...(cause && { cause }),
|
|
95
97
|
});
|
|
96
98
|
this.code = info.code;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
1
|
+
import { tenantDbRunner, tenantDbWriteRunner } from "../db/tenant-db-runner.js";
|
|
2
2
|
import { runProjectionsForEvent } from "../pipeline/projections-runner.js";
|
|
3
3
|
import { append, getStreamVersion } from "./event-store.js";
|
|
4
4
|
// Appends on the connection or transaction the TenantDb is bound to. The tenant is always the
|
|
5
5
|
// TenantDb's own, so a caller cannot write into another tenant's stream; a TenantDb the framework
|
|
6
6
|
// did not build (or a fail-closed systemScope guard) is rejected by tenantDbRunner.
|
|
7
7
|
export async function appendEventInTenantDb(tenantDb, event, options = {}) {
|
|
8
|
-
const runner =
|
|
8
|
+
const runner = tenantDbWriteRunner(tenantDb, "appendEventInTenantDb");
|
|
9
9
|
const stored = await append(runner, { ...event, tenantId: tenantDb.tenantId });
|
|
10
10
|
if (options.registry)
|
|
11
11
|
await runProjectionsForEvent(stored, options.registry, runner);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
1
|
+
import { type Registry, type SaveContext } from "../engine/types/index.js";
|
|
2
|
+
import type { FileRefRows } from "./load-save-file-refs.js";
|
|
3
3
|
export declare function assertFileRefsBelongToTenant(args: {
|
|
4
4
|
readonly registry: Registry;
|
|
5
5
|
readonly result: SaveContext;
|
|
6
|
-
readonly
|
|
7
|
-
|
|
8
|
-
}): Promise<void>;
|
|
6
|
+
readonly fileRefRows: FileRefRows;
|
|
7
|
+
}): void;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { isFileField
|
|
1
|
+
import { isFileField } from "../engine/types/index.js";
|
|
2
2
|
import { ValidationError } from "../errors/index.js";
|
|
3
3
|
import { fileRefIdsOf } from "./bind-pending-file-refs.js";
|
|
4
|
-
import { getFileRefExecutor } from "./create-file-ref.js";
|
|
5
4
|
import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
|
|
6
5
|
// A file field only stores an id, and the file routes authorize by tenant at
|
|
7
6
|
// read time: without this check a writer could name another tenant's fileRef
|
|
8
7
|
// and make the record point at it. Only newly written values are checked (all
|
|
9
8
|
// of a create, `changes` of an update), so existing data is left alone. The
|
|
10
9
|
// error is the same for "missing" and "belongs to another tenant" so the id
|
|
11
|
-
// cannot be probed across tenants.
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// cannot be probed across tenants. `fileRefRows` holds only the writer's own
|
|
11
|
+
// tenant's live rows, so a missing entry is exactly those two cases. Values
|
|
12
|
+
// that are not UUIDs are skipped by fileRefIdsOf and never reach this check.
|
|
13
|
+
export function assertFileRefsBelongToTenant(args) {
|
|
14
|
+
const { registry, result, fileRefRows } = args;
|
|
14
15
|
// skip: saves of unregistered aggregates (custom executors) declare no file fields
|
|
15
16
|
if (result.entityName === undefined)
|
|
16
17
|
return;
|
|
@@ -21,12 +22,11 @@ export async function assertFileRefsBelongToTenant(args) {
|
|
|
21
22
|
// skip: without the files feature the file_refs table is not part of the schema
|
|
22
23
|
if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined)
|
|
23
24
|
return;
|
|
24
|
-
const executor = getFileRefExecutor();
|
|
25
25
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
26
26
|
if (!isFileField(fieldDef))
|
|
27
27
|
continue;
|
|
28
28
|
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
29
|
-
if (
|
|
29
|
+
if (fileRefRows.has(fileRefId))
|
|
30
30
|
continue;
|
|
31
31
|
throw new ValidationError({
|
|
32
32
|
fields: [
|
|
@@ -40,11 +40,3 @@ export async function assertFileRefsBelongToTenant(args) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
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,9 +1,11 @@
|
|
|
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
|
+
import type { FileRefRows } from "./load-save-file-refs.js";
|
|
3
4
|
export declare function fileRefIdsOf(value: unknown): readonly string[];
|
|
4
5
|
export declare function bindPendingFileRefs(args: {
|
|
5
6
|
readonly registry: Registry;
|
|
6
7
|
readonly result: SaveContext;
|
|
7
8
|
readonly user: SessionUser;
|
|
8
9
|
readonly db: TenantDb;
|
|
10
|
+
readonly fileRefRows: FileRefRows;
|
|
9
11
|
}): Promise<void>;
|
|
@@ -23,7 +23,7 @@ function isPendingUploadFor(row, entityName, fieldName, user) {
|
|
|
23
23
|
// someone else's upload id in a save payload must not pull that file into a
|
|
24
24
|
// record the caller controls.
|
|
25
25
|
export async function bindPendingFileRefs(args) {
|
|
26
|
-
const { registry, result, user, db } = args;
|
|
26
|
+
const { registry, result, user, db, fileRefRows } = args;
|
|
27
27
|
// skip: saves of unnamed aggregates declare no file fields
|
|
28
28
|
if (result.entityName === undefined)
|
|
29
29
|
return;
|
|
@@ -42,8 +42,8 @@ export async function bindPendingFileRefs(args) {
|
|
|
42
42
|
// changes, not data: an update's row carries every stored file id, including
|
|
43
43
|
// ones bound earlier; only the ids this save names can be pending uploads.
|
|
44
44
|
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
45
|
-
const row =
|
|
46
|
-
if (row ===
|
|
45
|
+
const row = fileRefRows.get(fileRefId);
|
|
46
|
+
if (row === undefined || !isPendingUploadFor(row, entityName, fieldName, user))
|
|
47
47
|
continue;
|
|
48
48
|
const version = typeof row["version"] === "number" ? row["version"] : undefined;
|
|
49
49
|
const bound = await executor.update({ id: fileRefId, version, changes: { entityId: String(result.id) } }, user, db);
|
|
@@ -13,4 +13,4 @@ export declare const bindReferencedFilesWrite: import("@cosmicdrift/kumiko-types
|
|
|
13
13
|
bound: number;
|
|
14
14
|
skipped: number;
|
|
15
15
|
nextAfterId: string | null;
|
|
16
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
16
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|