@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -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 +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -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 +215 -0
- package/agents/examples/ledger-invoicing.md +236 -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 +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -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 +250 -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 +418 -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 +75 -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 +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- 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/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- 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/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- 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 +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -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 +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- 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 +8 -8
- 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/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -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 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- 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 +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- 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 +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- 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/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -80,13 +80,20 @@ export const softDeleteCleanupJob = async (_payload, ctx) => {
|
|
|
80
80
|
if (isSystemScopedSoftDeleteTable(proj.table, entity))
|
|
81
81
|
continue;
|
|
82
82
|
const where = { isDeleted: true, deletedAt: { lt: cutoff }, tenantId };
|
|
83
|
-
await purgeExpiredRows(db, proj.source, proj.table, where, ctx);
|
|
83
|
+
await purgeExpiredRows(db, proj.source, proj.table, where, cutoff, ctx);
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
-
async function purgeExpiredRows(db, source, table, where, ctx) {
|
|
86
|
+
async function purgeExpiredRows(db, source, table, where, cutoff, ctx) {
|
|
87
87
|
if (source === FILE_REF_ENTITY_NAME) {
|
|
88
88
|
// The row is the only record of the storageKey, so its bytes go first.
|
|
89
|
-
await purgeExpiredFileRefsWithBytes({
|
|
89
|
+
await purgeExpiredFileRefsWithBytes({
|
|
90
|
+
db,
|
|
91
|
+
table,
|
|
92
|
+
where,
|
|
93
|
+
expiredBefore: cutoff,
|
|
94
|
+
files: ctx.files,
|
|
95
|
+
log: ctx.log,
|
|
96
|
+
});
|
|
90
97
|
}
|
|
91
98
|
else {
|
|
92
99
|
await deleteMany(db, table, where);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unsafeRawForDeclaredStep } from "../../db/tenant-db.js";
|
|
1
|
+
import { requestedTenantIds, unsafeRawForDeclaredStep } from "../../db/tenant-db.js";
|
|
2
2
|
import { InternalError } from "../../errors/classes.js";
|
|
3
3
|
import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
|
|
4
4
|
// unsafeRaw fails closed without the handler's escapeHatch (or systemScope) and reports the use.
|
|
@@ -25,7 +25,10 @@ export function warnOnNarrowedForeignTenantFilter(ctx, stepKind, stepName, where
|
|
|
25
25
|
if (requested === undefined)
|
|
26
26
|
return;
|
|
27
27
|
const allowed = [ctx.db.tenantId, SYSTEM_TENANT_ID];
|
|
28
|
-
const requestedList =
|
|
28
|
+
const requestedList = requestedTenantIds(requested);
|
|
29
|
+
// skip: unrecognised operator form, narrowing cannot be judged.
|
|
30
|
+
if (requestedList === undefined)
|
|
31
|
+
return;
|
|
29
32
|
// skip: every requested tenant is within the caller's scope.
|
|
30
33
|
if (requestedList.every((t) => typeof t === "string" && allowed.includes(t)))
|
|
31
34
|
return;
|
|
@@ -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) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { extractTableName } from "../../db/index.js";
|
|
14
14
|
import { executeRawQuery } from "../../db/queries/raw-sql.js";
|
|
15
15
|
import { columnSqlNameOf } from "../../db/table-introspection.js";
|
|
16
|
-
import {
|
|
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) {
|
|
@@ -54,7 +54,7 @@ defineStep({
|
|
|
54
54
|
}
|
|
55
55
|
const sqlText = `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
|
|
56
56
|
`ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
|
|
57
|
-
await executeRawQuery(
|
|
57
|
+
await executeRawQuery(tenantDbWriteRunner(ctx.db, "unsafe projection upsert"), sqlText, params);
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
export function buildUnsafeProjectionUpsertStep(args) {
|
|
@@ -11,6 +11,9 @@ export declare const templateVariable: {
|
|
|
11
11
|
readonly link: (opts?: CommonOptions & {
|
|
12
12
|
readonly example?: string;
|
|
13
13
|
}) => TemplateScalarVariableDef;
|
|
14
|
+
readonly richText: (opts?: CommonOptions & {
|
|
15
|
+
readonly example?: string;
|
|
16
|
+
}) => TemplateScalarVariableDef;
|
|
14
17
|
readonly money: (opts?: CommonOptions & {
|
|
15
18
|
readonly example?: number | {
|
|
16
19
|
readonly amount: number;
|
|
@@ -10,6 +10,7 @@ export const templateVariable = {
|
|
|
10
10
|
type: "link",
|
|
11
11
|
...opts,
|
|
12
12
|
}),
|
|
13
|
+
richText: (opts = {}) => ({ type: "richText", ...opts }),
|
|
13
14
|
money: (opts = {}) => ({ type: "money", ...opts }),
|
|
14
15
|
date: (opts = {}) => ({
|
|
15
16
|
type: "date",
|
|
@@ -39,6 +40,8 @@ function scalarToZod(def) {
|
|
|
39
40
|
switch (def.type) {
|
|
40
41
|
case "text":
|
|
41
42
|
return z.string();
|
|
43
|
+
case "richText":
|
|
44
|
+
return z.string();
|
|
42
45
|
case "link":
|
|
43
46
|
return linkSchema;
|
|
44
47
|
case "money":
|
|
@@ -61,4 +61,5 @@ export type TierResolverPlugin = {
|
|
|
61
61
|
* plugin shape — the only thing every implementation must provide is the
|
|
62
62
|
* `build` factory. */
|
|
63
63
|
export declare function isTierResolverPlugin(v: unknown): v is TierResolverPlugin;
|
|
64
|
+
export declare function requireTierResolverPlugin(usage: RegistrarExtensionRegistration): TierResolverPlugin;
|
|
64
65
|
export declare function findTierResolverUsage(features: readonly FeatureDefinition[]): RegistrarExtensionRegistration | undefined;
|
|
@@ -42,6 +42,12 @@ export const TENANT_TIER_RESOLVER_EXT = "tenantTierResolver";
|
|
|
42
42
|
export function isTierResolverPlugin(v) {
|
|
43
43
|
return isPlainObject(v) && typeof v["build"] === "function";
|
|
44
44
|
}
|
|
45
|
+
export function requireTierResolverPlugin(usage) {
|
|
46
|
+
if (!isTierResolverPlugin(usage.options)) {
|
|
47
|
+
throw new Error("tier-resolver registration options must provide a build() factory");
|
|
48
|
+
}
|
|
49
|
+
return usage.options;
|
|
50
|
+
}
|
|
45
51
|
export function findTierResolverUsage(features) {
|
|
46
52
|
for (const feature of features) {
|
|
47
53
|
for (const usage of feature.extensionUsages) {
|
|
@@ -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, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, 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";
|
|
@@ -3,7 +3,7 @@ import type { AuthRoutesConfig } from "../api/auth-routes.js";
|
|
|
3
3
|
import type { JwtHelper, JwtKeyring } from "../api/jwt.js";
|
|
4
4
|
import type { ServerOptions } from "../api/server.js";
|
|
5
5
|
import type { SseBroker } from "../api/sse-broker.js";
|
|
6
|
-
import type { PgClient } from "../db/connection.js";
|
|
6
|
+
import type { PgClient, PgListenClient } from "../db/connection.js";
|
|
7
7
|
import type { AppContext, Registry } from "../engine/types/index.js";
|
|
8
8
|
import type { JobRunner, JobRunnerOptions } from "../jobs/job-runner.js";
|
|
9
9
|
import type { Lifecycle } from "../lifecycle/index.js";
|
|
@@ -82,4 +82,4 @@ export type AllInOneEntrypoint = Omit<ApiEntrypoint, "mode"> & Omit<WorkerEntryp
|
|
|
82
82
|
export declare function createApiEntrypoint(options: ApiEntrypointOptions): ApiEntrypoint;
|
|
83
83
|
export declare function createWorkerEntrypoint(options: WorkerEntrypointOptions): WorkerEntrypoint;
|
|
84
84
|
export declare function createAllInOneEntrypoint(options: AllInOneEntrypointOptions): AllInOneEntrypoint;
|
|
85
|
-
export type { PgClient };
|
|
85
|
+
export type { PgClient, PgListenClient };
|
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;
|
package/dist/errors/docs-url.js
CHANGED
|
@@ -20,17 +20,17 @@ function isCoveredByConfig(reason, errorDocs) {
|
|
|
20
20
|
function errorsUrl(baseUrl, slug) {
|
|
21
21
|
return `${baseUrl}/errors/${encodeURIComponent(slug)}`;
|
|
22
22
|
}
|
|
23
|
-
// A reason the framework documents (or the
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// A reason the framework documents (or no reason, or the code itself) links to the framework docs, overridable via
|
|
24
|
+
// KUMIKO_DOCS_URL. An app's own reason links to the app docs where `errorDocs` covers it; every
|
|
25
|
+
// other reason falls back to the framework page of the error code, which exists for each code.
|
|
26
26
|
export function resolveErrorDocsUrl(err, errorDocs) {
|
|
27
27
|
const reason = reasonOf(err);
|
|
28
|
+
const frameworkBaseUrl = process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
|
|
28
29
|
if (reason === undefined || reason === err.code || frameworkReasons.has(reason)) {
|
|
29
|
-
|
|
30
|
-
return errorsUrl(baseUrl, reason ?? err.code);
|
|
30
|
+
return errorsUrl(frameworkBaseUrl, reason ?? err.code);
|
|
31
31
|
}
|
|
32
32
|
if (errorDocs && isCoveredByConfig(reason, errorDocs)) {
|
|
33
33
|
return errorsUrl(errorDocs.baseUrl, reason);
|
|
34
34
|
}
|
|
35
|
-
return
|
|
35
|
+
return errorsUrl(frameworkBaseUrl, err.code);
|
|
36
36
|
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { AgentReason, FrameworkReason } from "./reasons.js";
|
|
|
9
9
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
10
10
|
export type { ErrorLogEntry, ErrorResponseBody } from "./serialize.js";
|
|
11
11
|
export { buildErrorLog, serializeError } from "./serialize.js";
|
|
12
|
-
export { subjectErasedConflict } from "./subject-erased.js";
|
|
12
|
+
export { isSubjectErasedConflict, subjectErasedConflict } from "./subject-erased.js";
|
|
13
13
|
export { toKumikoError } from "./to-kumiko-error.js";
|
|
14
14
|
export type { InvalidTransitionDetails } from "./transition-details.js";
|
|
15
15
|
export { buildInvalidTransitionDetails } from "./transition-details.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
|
4
4
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
5
5
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
6
6
|
export { buildErrorLog, serializeError } from "./serialize.js";
|
|
7
|
-
export { subjectErasedConflict } from "./subject-erased.js";
|
|
7
|
+
export { isSubjectErasedConflict, subjectErasedConflict } from "./subject-erased.js";
|
|
8
8
|
export { toKumikoError } from "./to-kumiko-error.js";
|
|
9
9
|
export { buildInvalidTransitionDetails } from "./transition-details.js";
|
|
10
10
|
export { failNotFound, failTransition, failUnprocessable, reraiseAsKumikoError, toWriteErrorInfo, writeFailure, } from "./write-error-info.js";
|
|
@@ -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) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { ConflictError } from "./classes.js";
|
|
2
3
|
import { FrameworkReasons } from "./reasons.js";
|
|
3
4
|
export function subjectErasedConflict(cause) {
|
|
@@ -7,3 +8,8 @@ export function subjectErasedConflict(cause) {
|
|
|
7
8
|
...(cause instanceof Error && { cause }),
|
|
8
9
|
});
|
|
9
10
|
}
|
|
11
|
+
export function isSubjectErasedConflict(e) {
|
|
12
|
+
return (e instanceof ConflictError &&
|
|
13
|
+
isPlainObject(e.details) &&
|
|
14
|
+
e.details["reason"] === FrameworkReasons.subjectErased);
|
|
15
|
+
}
|
|
@@ -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;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Guard-Script `scripts/guard-admin-api.ts` blockt Aufrufe aus App-Code —
|
|
8
8
|
// Allowlist: samples/*/migration/, scripts/migrations/, die Definition
|
|
9
9
|
// selbst, das Guard-Script selbst.
|
|
10
|
+
import { instantToTimestamptzParam } from "../bun-db/query.js";
|
|
10
11
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
11
12
|
import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
|
|
12
13
|
import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
|
|
@@ -53,7 +54,7 @@ function rawEventParams(event, newVersion, eventVersion) {
|
|
|
53
54
|
eventVersion,
|
|
54
55
|
payload: event.payload,
|
|
55
56
|
metadata: event.metadata,
|
|
56
|
-
createdAt: event.createdAt
|
|
57
|
+
createdAt: instantToTimestamptzParam(event.createdAt),
|
|
57
58
|
createdBy: event.createdBy,
|
|
58
59
|
};
|
|
59
60
|
}
|
|
@@ -88,7 +89,7 @@ export async function appendRawBatch(runner, events) {
|
|
|
88
89
|
const newVersion = e.expectedVersion + 1;
|
|
89
90
|
const eventVersion = e.eventVersion ?? 1;
|
|
90
91
|
const baseIdx = params.length;
|
|
91
|
-
params.push(e.aggregateId, e.aggregateType, e.tenantId, newVersion, e.type, eventVersion, e.payload, e.metadata, e.createdAt
|
|
92
|
+
params.push(e.aggregateId, e.aggregateType, e.tenantId, newVersion, e.type, eventVersion, e.payload, e.metadata, instantToTimestamptzParam(e.createdAt), e.createdBy);
|
|
92
93
|
return `($${baseIdx + 1}::uuid, $${baseIdx + 2}, $${baseIdx + 3}::uuid, $${baseIdx + 4}, $${baseIdx + 5}, $${baseIdx + 6}, $${baseIdx + 7}::jsonb, $${baseIdx + 8}::jsonb, $${baseIdx + 9}::timestamptz, $${baseIdx + 10})`;
|
|
93
94
|
});
|
|
94
95
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
export declare class VersionConflictError extends Error {
|
|
2
3
|
readonly aggregateId: string;
|
|
3
4
|
readonly expectedVersion: number;
|
|
@@ -8,6 +9,14 @@ export declare class IdempotentAppendConflictError extends Error {
|
|
|
8
9
|
readonly idempotencyKey: string;
|
|
9
10
|
constructor(tenantId: string, idempotencyKey: string);
|
|
10
11
|
}
|
|
12
|
+
export declare class SeedTimeOrderError extends Error {
|
|
13
|
+
readonly aggregateType: string;
|
|
14
|
+
readonly aggregateId: string;
|
|
15
|
+
readonly version: number;
|
|
16
|
+
readonly seedCreatedAt: Temporal.Instant;
|
|
17
|
+
readonly predecessorCreatedAt: Temporal.Instant;
|
|
18
|
+
constructor(aggregateType: string, aggregateId: string, version: number, seedCreatedAt: Temporal.Instant, predecessorCreatedAt: Temporal.Instant);
|
|
19
|
+
}
|
|
11
20
|
export declare class ArchivedStreamError extends Error {
|
|
12
21
|
readonly tenantId: string;
|
|
13
22
|
readonly aggregateId: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
// Failure modes of the event-store's append() path. Surfaced as typed
|
|
2
3
|
// errors so the executor layer can map them to the framework's
|
|
3
4
|
// WriteResult error contract (version_conflict).
|
|
@@ -31,6 +32,25 @@ export class IdempotentAppendConflictError extends Error {
|
|
|
31
32
|
this.idempotencyKey = idempotencyKey;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
35
|
+
// Thrown when a back-dated seed write would put a stream's next version
|
|
36
|
+
// before its predecessor in time. loadAggregateAsOf and projection rebuilds
|
|
37
|
+
// order by created_at, so such a stream would read and replay wrongly.
|
|
38
|
+
export class SeedTimeOrderError extends Error {
|
|
39
|
+
aggregateType;
|
|
40
|
+
aggregateId;
|
|
41
|
+
version;
|
|
42
|
+
seedCreatedAt;
|
|
43
|
+
predecessorCreatedAt;
|
|
44
|
+
constructor(aggregateType, aggregateId, version, seedCreatedAt, predecessorCreatedAt) {
|
|
45
|
+
super(`Seed write for ${aggregateType} ${aggregateId} v${version} is dated ${seedCreatedAt.toString()}, before its predecessor (${predecessorCreatedAt.toString()}). Write a stream's events in ascending time.`);
|
|
46
|
+
this.name = "SeedTimeOrderError";
|
|
47
|
+
this.aggregateType = aggregateType;
|
|
48
|
+
this.aggregateId = aggregateId;
|
|
49
|
+
this.version = version;
|
|
50
|
+
this.seedCreatedAt = seedCreatedAt;
|
|
51
|
+
this.predecessorCreatedAt = predecessorCreatedAt;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
34
54
|
// Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
|
|
35
55
|
// are read-only — restoreStream() makes them writable again. The archive
|
|
36
56
|
// state is not carried on the events themselves; it lives on the sparse
|
|
@@ -20,6 +20,12 @@ export declare function loadAggregate(db: DbRunner, aggregateId: string, tenantI
|
|
|
20
20
|
readonly includeArchived?: boolean;
|
|
21
21
|
readonly aggregateType?: string;
|
|
22
22
|
}): Promise<readonly StoredEvent[]>;
|
|
23
|
+
export declare function loadAggregateForWriting(db: DbRunner, aggregateId: string, tenantId: TenantId, options: {
|
|
24
|
+
readonly aggregateType: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
readonly events: readonly StoredEvent[];
|
|
27
|
+
readonly streamVersion: number;
|
|
28
|
+
}>;
|
|
23
29
|
export declare function loadAggregateAsOf(db: DbRunner, aggregateId: string, tenantId: TenantId, asOf: Temporal.Instant, options?: {
|
|
24
30
|
readonly includeArchived?: boolean;
|
|
25
31
|
}): Promise<readonly StoredEvent[]>;
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
|
+
import { instantToTimestamptzParam } from "../bun-db/query.js";
|
|
5
6
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
6
7
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
7
8
|
import { claimXactId, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
8
9
|
import { insertOne, selectMany } from "../db/query.js";
|
|
9
10
|
import { isStreamArchived } from "./archive.js";
|
|
10
|
-
import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
|
|
11
|
+
import { IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
11
12
|
import { eventsTable } from "./events-schema.js";
|
|
12
13
|
import { toStoredEvent } from "./row-to-stored-event.js";
|
|
13
14
|
import { currentSeedCreatedAt } from "./seed-clock.js";
|
|
@@ -44,14 +45,16 @@ export async function append(db, event) {
|
|
|
44
45
|
});
|
|
45
46
|
const toStore = stampOrigin(payload === event.payload ? event : { ...event, payload });
|
|
46
47
|
const seedCreatedAt = currentSeedCreatedAt();
|
|
47
|
-
if (seedCreatedAt !== undefined && toStore.expectedVersion > 0) {
|
|
48
|
-
await assertSeedTimeNotBeforePredecessor(db, toStore, seedCreatedAt);
|
|
49
|
-
}
|
|
50
48
|
const newVersion = toStore.expectedVersion + 1;
|
|
51
49
|
const eventVersion = toStore.eventVersion ?? 1;
|
|
52
50
|
try {
|
|
53
51
|
const row = await runAtomicEventWrite(db, async (runner) => {
|
|
54
52
|
await claimXactId(runner);
|
|
53
|
+
// Inside the write transaction so a concurrent live append cannot slip
|
|
54
|
+
// between the check and the insert.
|
|
55
|
+
if (seedCreatedAt !== undefined && toStore.expectedVersion > 0) {
|
|
56
|
+
await assertSeedTimeNotBeforePredecessor(runner, toStore, seedCreatedAt);
|
|
57
|
+
}
|
|
55
58
|
const inserted = toStore.expectedVersion === 0
|
|
56
59
|
? await insertFirstEvent(runner, toStore, newVersion, eventVersion, seedCreatedAt)
|
|
57
60
|
: await insertSubsequentEvent(runner, toStore, newVersion, eventVersion, seedCreatedAt);
|
|
@@ -105,7 +108,7 @@ async function assertSeedTimeNotBeforePredecessor(db, event, seedCreatedAt) {
|
|
|
105
108
|
version: event.expectedVersion,
|
|
106
109
|
});
|
|
107
110
|
if (predecessor && predecessor.createdAt.epochMilliseconds > seedCreatedAt.epochMilliseconds) {
|
|
108
|
-
throw new
|
|
111
|
+
throw new SeedTimeOrderError(event.aggregateType, event.aggregateId, event.expectedVersion + 1, seedCreatedAt, predecessor.createdAt);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
async function insertFirstEvent(db, event, newVersion, eventVersion, seedCreatedAt) {
|
|
@@ -141,7 +144,7 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
141
144
|
metadata: event.metadata,
|
|
142
145
|
createdBy: event.metadata.userId,
|
|
143
146
|
expectedVersion: event.expectedVersion,
|
|
144
|
-
createdAt: seedCreatedAt
|
|
147
|
+
createdAt: seedCreatedAt && instantToTimestamptzParam(seedCreatedAt),
|
|
145
148
|
});
|
|
146
149
|
if (!row)
|
|
147
150
|
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
@@ -201,6 +204,25 @@ export async function loadAggregate(db, aggregateId, tenantId, options) {
|
|
|
201
204
|
: { aggregateId, tenantId }, { orderBy: { col: "version", direction: "asc" } });
|
|
202
205
|
return rows.map(toStoredEvent);
|
|
203
206
|
}
|
|
207
|
+
// Events and the stream's latest version from ONE read, so both come from the same
|
|
208
|
+
// snapshot: a commit landing between two separate statements would leave the caller
|
|
209
|
+
// pinning a version its loaded events never covered. The aggregateType filter is applied
|
|
210
|
+
// after the read because a foreign type sharing the aggregateId still occupies versions
|
|
211
|
+
// the next append has to stack on. Archived streams load empty but keep their real version.
|
|
212
|
+
export async function loadAggregateForWriting(db, aggregateId, tenantId, options) {
|
|
213
|
+
if (options.aggregateType === "") {
|
|
214
|
+
throw new Error("loadAggregateForWriting: aggregateType must be a non-empty string");
|
|
215
|
+
}
|
|
216
|
+
if (await isStreamArchived(db, tenantId, aggregateId)) {
|
|
217
|
+
return { events: [], streamVersion: await getStreamVersion(db, aggregateId, tenantId) };
|
|
218
|
+
}
|
|
219
|
+
const rows = await selectMany(db, eventsTable, { aggregateId, tenantId }, { orderBy: { col: "version", direction: "asc" } });
|
|
220
|
+
const all = rows.map(toStoredEvent);
|
|
221
|
+
return {
|
|
222
|
+
events: all.filter((e) => e.aggregateType === options.aggregateType),
|
|
223
|
+
streamVersion: all.at(-1)?.version ?? 0,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
204
226
|
// Load events up to a point in time. Used for asOf queries that reconstruct
|
|
205
227
|
// historical state. Includes events whose created_at is <= asOf. Same
|
|
206
228
|
// archive semantics as loadAggregate.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { APP_INSTANCE_STREAM_TYPE, APP_STARTED_EVENT_TYPE } from "../crypto/system-event-pii.js";
|
|
2
2
|
export { backfillEventPiiEncryption, backfillEventPiiEncryptionBatch, type PiiBackfillBatchOptions, type PiiBackfillBatchResult, type PiiBackfillFailure, type PiiBackfillOptions, type PiiBackfillResult, type PiiBackfillScanCache, } from "../db/queries/backfill-pii.js";
|
|
3
3
|
export { type ArchiveStreamArgs, archivedStreamsTable, archiveStream, createArchivedStreamsTable, isStreamArchived, restoreStream, } from "./archive.js";
|
|
4
|
-
export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError, } from "./errors.js";
|
|
4
|
+
export { ArchivedStreamError, IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
5
5
|
export { append, EVENTS_PUBSUB_CHANNEL, type EventMetadata, type EventToAppend, getEventsHighWaterMark, getStreamVersion, getStreamVersions, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, type StoredEvent, streamAllEventsByType, } from "./event-store.js";
|
|
6
6
|
export { createEventsTable, eventsTable } from "./events-schema.js";
|
|
7
7
|
export { appendProvenanceEvent, type ProvenanceEventInput } from "./provenance-append.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { APP_INSTANCE_STREAM_TYPE, APP_STARTED_EVENT_TYPE } from "../crypto/system-event-pii.js";
|
|
2
2
|
export { backfillEventPiiEncryption, backfillEventPiiEncryptionBatch, } from "../db/queries/backfill-pii.js";
|
|
3
3
|
export { archivedStreamsTable, archiveStream, createArchivedStreamsTable, isStreamArchived, restoreStream, } from "./archive.js";
|
|
4
|
-
export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError, } from "./errors.js";
|
|
4
|
+
export { ArchivedStreamError, IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
5
5
|
export { append, EVENTS_PUBSUB_CHANNEL, getEventsHighWaterMark, getStreamVersion, getStreamVersions, getUnscopedAggregateStreamMaxVersion, getUnscopedStreamMaxVersionForSeed, LOAD_ALL_EVENTS_ROW_LIMIT, loadAggregate, loadAggregateAsOf, loadAllEventsByType, loadEventsAfterVersion, streamAllEventsByType, } from "./event-store.js";
|
|
6
6
|
export { createEventsTable, eventsTable } from "./events-schema.js";
|
|
7
7
|
export { appendProvenanceEvent } from "./provenance-append.js";
|
|
@@ -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);
|