@cosmicdrift/kumiko-framework 0.387.0 → 0.388.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 +6 -1
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +1 -0
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.js +16 -12
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/dialect.js +1 -1
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +48 -701
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +395 -499
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +4 -1
- package/dist/db/query.d.ts +2 -15
- package/dist/db/query.js +8 -761
- package/dist/db/reference-data.js +6 -1
- package/dist/db/table-builder.js +113 -166
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.js +39 -9
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +12 -9
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.js +56 -96
- package/dist/engine/define-feature.js +9 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +9 -0
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +48 -87
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +7 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.js +10 -4
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.js +11 -8
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.js +7 -0
- package/dist/i18n/required-surface-keys.js +182 -206
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +61 -2
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +47 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +58 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +3 -5
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +2 -1
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/upgrade-cli.js +149 -121
- package/package.json +4 -4
- package/src/changes.json +310 -0
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
package/dist/entrypoint/index.js
CHANGED
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
// connected to any API instance via Redis Pub/Sub, no per-instance
|
|
41
41
|
// cursor needed. Without REDIS_URL, the in-memory broker still only
|
|
42
42
|
// reaches clients on the same process.
|
|
43
|
-
import { buildServer
|
|
43
|
+
import { buildServer } from "../api/server.js";
|
|
44
|
+
import { withFileProviderResolver } from "../api/server-wiring.js";
|
|
44
45
|
import { createJobRunner } from "../jobs/job-runner.js";
|
|
45
46
|
import { createLifecycle } from "../lifecycle/index.js";
|
|
46
47
|
import { createNoopProvider } from "../observability/index.js";
|
|
@@ -119,6 +120,8 @@ function buildApiServer(opts, lifecycle, dispatcherOverride, jobRunner, processL
|
|
|
119
120
|
jwtIssuer: opts.jwtIssuer,
|
|
120
121
|
auth: opts.auth,
|
|
121
122
|
trustedProxyHops: opts.trustedProxyHops,
|
|
123
|
+
errorDocs: opts.errorDocs,
|
|
124
|
+
instanceId: opts.instanceId,
|
|
122
125
|
anonymousAccess: opts.anonymousAccess,
|
|
123
126
|
sseBroker: opts.sseBroker,
|
|
124
127
|
rateLimit: opts.rateLimit,
|
|
@@ -145,6 +148,7 @@ function buildWorkerServer(opts, lifecycle, jobRunner) {
|
|
|
145
148
|
jwtSecret: opts.jwtSecret,
|
|
146
149
|
lifecycle,
|
|
147
150
|
processLane: "worker",
|
|
151
|
+
instanceId: opts.instanceId,
|
|
148
152
|
observability: opts.observability,
|
|
149
153
|
dispatcherOptions,
|
|
150
154
|
systemHooks: opts.systemHooks,
|
|
@@ -240,6 +244,7 @@ export function createApiEntrypoint(options) {
|
|
|
240
244
|
lifecycle,
|
|
241
245
|
observability: server.observability,
|
|
242
246
|
dispatcher: server.dispatcher,
|
|
247
|
+
lifecycleHooks: server.lifecycleHooks,
|
|
243
248
|
...(server.eventDispatcher && { eventDispatcher: server.eventDispatcher }),
|
|
244
249
|
mode: "api",
|
|
245
250
|
async start() {
|
|
@@ -319,6 +324,7 @@ export function createAllInOneEntrypoint(options) {
|
|
|
319
324
|
jobRunner: workerJobRunner,
|
|
320
325
|
observability: server.observability,
|
|
321
326
|
dispatcher: server.dispatcher,
|
|
327
|
+
lifecycleHooks: server.lifecycleHooks,
|
|
322
328
|
mode: "all-in-one",
|
|
323
329
|
async start() {
|
|
324
330
|
await eventDispatcher.start();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KumikoError } from "./kumiko-error.ts";
|
|
2
|
-
/** Links an app's own error reasons to its own docs;
|
|
2
|
+
/** Links an app's own error reasons to its own docs; uncovered reasons fall back to the framework page of the error code. */
|
|
3
3
|
export type ErrorDocsConfig = {
|
|
4
4
|
readonly baseUrl: string;
|
|
5
5
|
readonly reasons?: readonly string[] | "all";
|
package/dist/errors/docs-url.js
CHANGED
|
@@ -5,6 +5,26 @@ const frameworkReasons = new Set([
|
|
|
5
5
|
...Object.values(FrameworkReasons),
|
|
6
6
|
...Object.values(AgentReasons),
|
|
7
7
|
]);
|
|
8
|
+
// Codes of the built-in error classes: only these have a framework docs page.
|
|
9
|
+
const frameworkErrorCodes = new Set([
|
|
10
|
+
"validation_error",
|
|
11
|
+
"feature_disabled",
|
|
12
|
+
"access_denied",
|
|
13
|
+
"unauthenticated",
|
|
14
|
+
"not_found",
|
|
15
|
+
"conflict",
|
|
16
|
+
"version_conflict",
|
|
17
|
+
"precondition_failed",
|
|
18
|
+
"record_locked",
|
|
19
|
+
"idempotent_replay",
|
|
20
|
+
"unique_violation",
|
|
21
|
+
"unprocessable",
|
|
22
|
+
"unconfigured",
|
|
23
|
+
"internal_error",
|
|
24
|
+
"member_read_timeout",
|
|
25
|
+
"rate_limited",
|
|
26
|
+
"rate_limit_unavailable",
|
|
27
|
+
]);
|
|
8
28
|
function reasonOf(err) {
|
|
9
29
|
if (!err.details || typeof err.details !== "object")
|
|
10
30
|
return undefined;
|
|
@@ -22,7 +42,7 @@ function errorsUrl(baseUrl, slug) {
|
|
|
22
42
|
}
|
|
23
43
|
// A reason the framework documents (or no reason, or the code itself) links to the framework docs, overridable via
|
|
24
44
|
// 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,
|
|
45
|
+
// other reason falls back to the framework page of the error code, unless an app subclass overrode the code.
|
|
26
46
|
export function resolveErrorDocsUrl(err, errorDocs) {
|
|
27
47
|
const reason = reasonOf(err);
|
|
28
48
|
const frameworkBaseUrl = process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
|
|
@@ -32,5 +52,5 @@ export function resolveErrorDocsUrl(err, errorDocs) {
|
|
|
32
52
|
if (errorDocs && isCoveredByConfig(reason, errorDocs)) {
|
|
33
53
|
return errorsUrl(errorDocs.baseUrl, reason);
|
|
34
54
|
}
|
|
35
|
-
return errorsUrl(frameworkBaseUrl, err.code);
|
|
55
|
+
return frameworkErrorCodes.has(err.code) ? errorsUrl(frameworkBaseUrl, err.code) : undefined;
|
|
36
56
|
}
|
package/dist/es-ops/context.js
CHANGED
|
@@ -43,7 +43,7 @@ export function createSeedMigrationContext(args) {
|
|
|
43
43
|
if (!result.isSuccess) {
|
|
44
44
|
const code = result.error?.code ?? "unknown";
|
|
45
45
|
const message = result.error?.message ?? "(no message)";
|
|
46
|
-
throw new Error(`
|
|
46
|
+
throw new Error(`systemWriteAs("${handlerQualifiedName}") failed: ${code} — ${message}`);
|
|
47
47
|
}
|
|
48
48
|
return result;
|
|
49
49
|
},
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// path `@cosmicdrift/kumiko-framework/event-store/admin-api`. The guard script
|
|
7
7
|
// `scripts/guard-admin-api.ts` blocks calls from application code. Allowlist: samples/*/migration/,
|
|
8
8
|
// scripts/migrations/, this definition, and the guard script itself.
|
|
9
|
+
import { instantToTimestamptzParam } from "../db/driver-value-preparation.js";
|
|
9
10
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
10
11
|
import { claimXactId, runAtomicEventWrite } from "../db/queries/event-store.js";
|
|
11
12
|
import { eventPredecessorExists, findExistingEventVersion, insertRawEventBatch, insertRawFirstEvent, insertRawSubsequentEvent, } from "../db/queries/event-store-admin.js";
|
|
12
|
-
import { instantToTimestamptzParam } from "../db/query.js";
|
|
13
13
|
import { IdempotentAppendConflictError, VersionConflictError } from "./errors.js";
|
|
14
14
|
function mapEventUniqueViolation(e, event) {
|
|
15
15
|
if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
|
|
@@ -3,9 +3,10 @@ import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-t
|
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
import { requestContext } from "../api/request-context.js";
|
|
5
5
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
6
|
+
import { instantToTimestamptzParam } from "../db/driver-value-preparation.js";
|
|
6
7
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
7
8
|
import { claimXactId, insertFirstEventRow, insertSubsequentEventRow, notifyPgChannel, runAtomicEventWrite, selectAggregateMaxVersion, selectEventsHighWaterMark, selectStreamMaxVersion, selectStreamMaxVersions, } from "../db/queries/event-store.js";
|
|
8
|
-
import {
|
|
9
|
+
import { selectMany } from "../db/query.js";
|
|
9
10
|
import { isStreamArchived } from "./archive.js";
|
|
10
11
|
import { IdempotentAppendConflictError, SeedTimeOrderError, VersionConflictError, } from "./errors.js";
|
|
11
12
|
import { eventsTable } from "./events-schema.js";
|
|
@@ -14,8 +15,11 @@ import { currentSeedCreatedAt } from "./seed-clock.js";
|
|
|
14
15
|
// Append one event atomically. Two guarantees combined:
|
|
15
16
|
//
|
|
16
17
|
// 1. UNIQUE (tenant_id, aggregate_id, version) serializes concurrent writers
|
|
17
|
-
// within a tenant
|
|
18
|
-
//
|
|
18
|
+
// within a tenant. A second writer racing the same expectedVersion hits
|
|
19
|
+
// ON CONFLICT DO NOTHING (no row), then the stream version is read inside
|
|
20
|
+
// the still-intact transaction → VersionConflictError. That relies on
|
|
21
|
+
// READ COMMITTED; REPEATABLE READ/SERIALIZABLE raise 40001 instead. Only
|
|
22
|
+
// the idempotency index still raises 23505.
|
|
19
23
|
// Cross-tenant aggregate_id collisions are not conflicts by definition:
|
|
20
24
|
// two tenants owning a row with the same UUID is just isolation, not a
|
|
21
25
|
// race.
|
|
@@ -28,7 +32,7 @@ import { currentSeedCreatedAt } from "./seed-clock.js";
|
|
|
28
32
|
// stays semantically obvious when read in isolation.
|
|
29
33
|
//
|
|
30
34
|
// Creates (expectedVersion === 0) skip the predecessor check — no predecessor
|
|
31
|
-
// exists yet. Colliding creates fall out via
|
|
35
|
+
// exists yet. Colliding creates fall out via the same index (tenant_id, aggregate_id, version=1).
|
|
32
36
|
// Channel name used by append() → NOTIFY and the event-dispatcher → LISTEN
|
|
33
37
|
// (Sprint E.4). The event-dispatcher subscribes to this channel on start and
|
|
34
38
|
// fires a runOnce immediately on each commit, so delivery latency is bounded
|
|
@@ -66,6 +70,8 @@ export async function append(db, event) {
|
|
|
66
70
|
}
|
|
67
71
|
catch (e) {
|
|
68
72
|
if (isUniqueViolation(e)) {
|
|
73
|
+
// The version index normally never throws (ON CONFLICT DO NOTHING); it
|
|
74
|
+
// only lands here for an unknown or renamed constraint.
|
|
69
75
|
// Two unique constraints on this table: events_aggregate_version_uq
|
|
70
76
|
// (tenant_id, aggregate_id, version) — a concurrent writer won the
|
|
71
77
|
// race to the next version — and events_idempotency_uq (tenant_id,
|
|
@@ -13,6 +13,9 @@ export function fileRefIdsOf(value) {
|
|
|
13
13
|
return value.filter((item) => typeof item === "string" && isUuid(item));
|
|
14
14
|
}
|
|
15
15
|
function isPendingUploadFor(row, entityName, fieldName, user) {
|
|
16
|
+
// Every anonymous visitor shares one user id, so insertedById cannot tell their uploads apart.
|
|
17
|
+
if (isAnonymousSessionUser(user))
|
|
18
|
+
return false;
|
|
16
19
|
return (row["entityId"] === null &&
|
|
17
20
|
row["entityType"] === entityName &&
|
|
18
21
|
row["fieldName"] === fieldName &&
|
|
@@ -79,9 +82,6 @@ export async function bindPendingFileRefs(args) {
|
|
|
79
82
|
}
|
|
80
83
|
async function loadPendingRow(lookup) {
|
|
81
84
|
const { fileRefId, entityName, fieldName, user, db } = lookup;
|
|
82
|
-
// Every anonymous visitor shares one user id, so insertedById cannot tell their uploads apart.
|
|
83
|
-
if (isAnonymousSessionUser(user))
|
|
84
|
-
return undefined;
|
|
85
85
|
if (!isUuid(fileRefId))
|
|
86
86
|
return undefined;
|
|
87
87
|
const row = (await loadFileRefRows(db, [fileRefId], user.tenantId)).get(fileRefId);
|
|
@@ -27,7 +27,10 @@ export async function createFileRefFromBytes(ctx, input) {
|
|
|
27
27
|
registry: ctx.registry,
|
|
28
28
|
defaultMaxUploadSize: readFilesRouteOptions(ctx.registry).maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
|
|
29
29
|
writeBytes: (key, data, mimeType) => files.writeUploadBytes(key, data, mimeType),
|
|
30
|
-
deleteBytes: (key) =>
|
|
30
|
+
deleteBytes: async (key) => {
|
|
31
|
+
await files.ref(key).delete();
|
|
32
|
+
await files.forgetUploadBytes(key);
|
|
33
|
+
},
|
|
31
34
|
}, {
|
|
32
35
|
fileName: input.fileName,
|
|
33
36
|
declaredMimeType: input.mimeType,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// suffix before the file extension — `foo/bar.jpg` + `"medium-<16 hex>"` →
|
|
11
11
|
// `foo/bar.medium-<16 hex>.jpg`. The suffix must be a `variantSuffix(...)` so
|
|
12
12
|
// the erasure sweep recognizes it. Stable, reversible, no extra lookup tables.
|
|
13
|
-
import { isDerivativeSuffix } from "../derivatives/variant-key.js";
|
|
13
|
+
import { derivativeListPrefix, isDerivativeSuffix } from "../derivatives/variant-key.js";
|
|
14
14
|
import { InternalError } from "../errors/index.js";
|
|
15
15
|
import { assertSafeStorageKey } from "./types.js";
|
|
16
16
|
// `getProvider` is a lazily-resolved, memoized accessor — the provider is
|
|
@@ -127,6 +127,5 @@ export function deriveKey(key, suffix) {
|
|
|
127
127
|
// fileRef's original plus every already-rendered derivative, none of which have their own
|
|
128
128
|
// `file_refs` row.
|
|
129
129
|
export function storageKeyStemPrefix(key) {
|
|
130
|
-
|
|
131
|
-
return ext === "" ? `${key}.` : `${stem}.`;
|
|
130
|
+
return derivativeListPrefix(key);
|
|
132
131
|
}
|
|
@@ -12,13 +12,13 @@ import { isFileField, isUuid, } from "../engine/types/index.js";
|
|
|
12
12
|
import { listActionFormFileFields, mayUseActionForm } from "./action-form-upload.js";
|
|
13
13
|
import { buildContentDispositionHeader } from "./content-disposition.js";
|
|
14
14
|
import { DEFAULT_MAX_UPLOAD_SIZE, getFileRefExecutor, storeFileAsFileRef, } from "./create-file-ref.js";
|
|
15
|
-
import { createFileContext } from "./file-handle.js";
|
|
15
|
+
import { createFileContext, createTenantFileContext } from "./file-handle.js";
|
|
16
16
|
import { fileRefEntity } from "./file-ref-entity.js";
|
|
17
17
|
import { fileRefsTable } from "./file-ref-table.js";
|
|
18
18
|
import { RecordWriteDecisions } from "./record-write-check.js";
|
|
19
19
|
import { neutralFileNameForMimeType } from "./resolve-content-type.js";
|
|
20
20
|
import { parseMaxSize, resolveServedContentType } from "./types.js";
|
|
21
|
-
import {
|
|
21
|
+
import { withUploadLedger } from "./upload-ledger.js";
|
|
22
22
|
// Exactly the httpStatus values declared by KumikoError subclasses across packages/*.
|
|
23
23
|
const ERROR_STATUSES = [400, 401, 403, 404, 409, 422, 429, 500, 503, 504];
|
|
24
24
|
export function deleteFailureResponseFor(error) {
|
|
@@ -102,6 +102,10 @@ export function readFilesRouteOptions(registry) {
|
|
|
102
102
|
}
|
|
103
103
|
export function createFileRoutes(options) {
|
|
104
104
|
const { db } = options;
|
|
105
|
+
// Plain resolvers (not built via withFileProviderResolver) get the ledger on the route's own root connection.
|
|
106
|
+
const resolveProvider = options.resolveProvider.uploadLedgerFor
|
|
107
|
+
? options.resolveProvider
|
|
108
|
+
: withUploadLedger(options.resolveProvider, db);
|
|
105
109
|
const privilegedRoles = options.privilegedRoles ?? DEFAULT_PRIVILEGED_ROLES;
|
|
106
110
|
const guard = options.accessGuard ??
|
|
107
111
|
createDefaultGuard(privilegedRoles, options.canReadRecord, options.unclaimedUploadReaders ?? []);
|
|
@@ -200,19 +204,18 @@ export function createFileRoutes(options) {
|
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
const data = new Uint8Array(await file.arrayBuffer());
|
|
203
|
-
const
|
|
204
|
-
const uploadLedger = createFileUploadLedger(db, user.tenantId);
|
|
207
|
+
const files = createTenantFileContext(resolveProvider, user.tenantId);
|
|
205
208
|
const outcome = await storeFileAsFileRef({
|
|
206
209
|
user,
|
|
207
210
|
tenantDb: tenantDbFor(user.tenantId),
|
|
208
211
|
rowInOwnTransaction: true,
|
|
209
212
|
registry: options.registry,
|
|
210
213
|
defaultMaxUploadSize: options.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
|
|
211
|
-
writeBytes:
|
|
212
|
-
|
|
213
|
-
await
|
|
214
|
+
writeBytes: (key, bytes, mimeType) => files.writeUploadBytes(key, bytes, mimeType),
|
|
215
|
+
deleteBytes: async (key) => {
|
|
216
|
+
await files.ref(key).delete();
|
|
217
|
+
await files.forgetUploadBytes(key);
|
|
214
218
|
},
|
|
215
|
-
deleteBytes: (key) => storageProvider.delete(key),
|
|
216
219
|
}, {
|
|
217
220
|
fileName: file.name,
|
|
218
221
|
declaredMimeType: file.type,
|
|
@@ -46,6 +46,49 @@ const NON_XML_ROOT_ELEMENTS = new Set([
|
|
|
46
46
|
"article",
|
|
47
47
|
"font",
|
|
48
48
|
"center",
|
|
49
|
+
"form",
|
|
50
|
+
"iframe",
|
|
51
|
+
"input",
|
|
52
|
+
"button",
|
|
53
|
+
"nav",
|
|
54
|
+
"header",
|
|
55
|
+
"footer",
|
|
56
|
+
"main",
|
|
57
|
+
"pre",
|
|
58
|
+
"code",
|
|
59
|
+
"b",
|
|
60
|
+
"i",
|
|
61
|
+
"strong",
|
|
62
|
+
"em",
|
|
63
|
+
"label",
|
|
64
|
+
"textarea",
|
|
65
|
+
"select",
|
|
66
|
+
"fieldset",
|
|
67
|
+
"legend",
|
|
68
|
+
"tr",
|
|
69
|
+
"td",
|
|
70
|
+
"th",
|
|
71
|
+
"thead",
|
|
72
|
+
"tbody",
|
|
73
|
+
"tfoot",
|
|
74
|
+
"object",
|
|
75
|
+
"embed",
|
|
76
|
+
"video",
|
|
77
|
+
"audio",
|
|
78
|
+
"canvas",
|
|
79
|
+
"math",
|
|
80
|
+
"picture",
|
|
81
|
+
"figure",
|
|
82
|
+
"figcaption",
|
|
83
|
+
"blockquote",
|
|
84
|
+
"dl",
|
|
85
|
+
"dt",
|
|
86
|
+
"dd",
|
|
87
|
+
"aside",
|
|
88
|
+
"details",
|
|
89
|
+
"summary",
|
|
90
|
+
"dialog",
|
|
91
|
+
"noscript",
|
|
49
92
|
]);
|
|
50
93
|
const ROOT_ELEMENT_PATTERN = /^<(?:[A-Za-z_][\w.-]*:)?([A-Za-z_][\w.-]*)/;
|
|
51
94
|
const EXTENSION_FALLBACK = {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineUnmanagedTable } from "../db/entity-table-meta.js";
|
|
2
|
+
import { runnerCanBeginTransaction } from "../db/query.js";
|
|
2
3
|
import { createTenantDb } from "../db/tenant-db.js";
|
|
3
4
|
import { tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
5
|
+
import { InternalError } from "../errors/index.js";
|
|
4
6
|
const CREATED_AT_INDEX = "store_file_upload_ledger_created_at_idx";
|
|
5
7
|
// One row per upload whose bytes are in storage but whose fileRef row is not committed yet. The
|
|
6
8
|
// commit path deletes it inside the transaction that creates the fileRef; the sweep finds the
|
|
@@ -21,6 +23,11 @@ function isTenantDb(db) {
|
|
|
21
23
|
// handler must not take the row with it, or the bytes it uploaded would be unfindable.
|
|
22
24
|
export function createFileUploadLedger(db, tenantId) {
|
|
23
25
|
const runner = isTenantDb(db) ? tenantDbRunner(db) : db;
|
|
26
|
+
if (!runnerCanBeginTransaction(runner)) {
|
|
27
|
+
throw new InternalError({
|
|
28
|
+
message: "createFileUploadLedger: the connection is already inside a transaction; the ledger row would roll back with it and the uploaded bytes become unfindable",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
24
31
|
const ledgerDb = createTenantDb({ db: runner, tenantId });
|
|
25
32
|
return {
|
|
26
33
|
record: async (storageKey) => {
|