@cosmicdrift/kumiko-framework 0.371.1 → 0.373.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 +4 -1
- package/agents/examples/custom-handlers.md +3 -5
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/pdf-audit-export.md +1 -1
- package/agents/examples/rate-limiting.md +65 -1
- package/agents/examples/user-data-rights.md +176 -0
- package/dist/api/jwt.js +2 -1
- package/dist/api/observability-middleware.d.ts +1 -2
- package/dist/api/observability-middleware.js +9 -8
- package/dist/api/pii-leak-guard.js +2 -1
- package/dist/api/route-label.d.ts +3 -0
- package/dist/api/route-label.js +17 -0
- package/dist/api/server.d.ts +1 -2
- package/dist/api/server.js +3 -5
- package/dist/config/index.d.ts +5 -0
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/located-timestamp.d.ts +7 -0
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +8 -2
- package/dist/db/table-builder.d.ts +45 -11
- package/dist/db/tenant-db.js +3 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +47 -16
- package/dist/engine/entity-handlers.d.ts +3 -3
- package/dist/engine/factories.d.ts +2 -1
- package/dist/engine/factories.js +2 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +8 -8
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +150 -6
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/membership-roles.d.ts +1 -0
- package/dist/engine/membership-roles.js +8 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +30 -2
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/types/index.js +1 -1
- package/dist/entrypoint/index.d.ts +1 -0
- package/dist/entrypoint/index.js +0 -2
- package/dist/files/assert-file-refs-belong-to-tenant.js +27 -1
- package/dist/files/bind-pending-file-refs.d.ts +31 -1
- package/dist/files/bind-pending-file-refs.js +80 -6
- package/dist/files/create-file-ref-from-bytes.js +12 -6
- package/dist/files/create-file-ref.d.ts +20 -2
- package/dist/files/create-file-ref.js +10 -1
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +25 -0
- package/dist/files/file-handle.d.ts +4 -1
- package/dist/files/file-handle.js +30 -4
- package/dist/files/file-ref-entity.d.ts +7 -7
- package/dist/files/file-ref-table.d.ts +7 -7
- package/dist/files/file-routes.js +6 -1
- package/dist/files/index.d.ts +5 -1
- package/dist/files/index.js +4 -1
- package/dist/files/load-save-file-refs.js +3 -1
- package/dist/files/provider-resolver.js +3 -1
- package/dist/files/sweep-orphaned-uploads.d.ts +16 -0
- package/dist/files/sweep-orphaned-uploads.js +53 -0
- package/dist/files/upload-ledger.d.ts +10 -0
- package/dist/files/upload-ledger.js +47 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/request-locale.d.ts +15 -4
- package/dist/i18n/request-locale.js +26 -9
- package/dist/jobs/job-runner.js +2 -4
- package/dist/pipeline/dispatch-shared.js +57 -39
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/rate-limit/index.d.ts +2 -1
- package/dist/rate-limit/index.js +1 -0
- package/dist/rate-limit/resolver.d.ts +1 -1
- package/dist/rate-limit/resolver.js +104 -2
- package/dist/rate-limit/unlimited-resolver.d.ts +2 -0
- package/dist/rate-limit/unlimited-resolver.js +22 -0
- package/dist/schema-cli.js +46 -5
- package/package.json +5 -5
- package/src/changes.json +55 -0
- package/src/scripts/codemod/README.md +22 -0
- package/src/scripts/codemod/remove-engine-payload-casts.ts +318 -0
|
@@ -7,8 +7,8 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
|
|
|
7
7
|
export type { BootCheckContext, BootCheckFn, BootOptionName, EntityObjectDefinition, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, HookDefinition, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StackBootRequirement, 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, 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
|
-
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } 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, RateLimitOptions, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
11
|
+
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, declaredRateLimitOptions, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isRateLimitOption, 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";
|
|
14
14
|
export type { EntityId, TenantId } from "@cosmicdrift/kumiko-types/identifiers";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Barrel: re-exports all types from @cosmicdrift/kumiko-types, plus the
|
|
2
2
|
// runtime helpers below that stay framework-side.
|
|
3
3
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
4
|
-
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
4
|
+
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, declaredRateLimitOptions, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isRateLimitOption, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
5
5
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-types/identifiers";
|
|
6
6
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
7
7
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, isFileField } from "../field-helpers.js";
|
|
@@ -17,6 +17,7 @@ export type BaseEntrypointOptions = {
|
|
|
17
17
|
readonly jwtSecret: string | JwtKeyring;
|
|
18
18
|
readonly jwtIssuer?: string;
|
|
19
19
|
readonly observability?: ObservabilityProvider;
|
|
20
|
+
/** @deprecated No effect: the HTTP route label carries no query string anymore, so there is nothing left to configure. */
|
|
20
21
|
readonly observabilityOptions?: ObservabilityOptions;
|
|
21
22
|
readonly dispatcherOptions?: Omit<DispatcherOptions, "lifecycle">;
|
|
22
23
|
readonly systemHooks?: SystemHooks;
|
package/dist/entrypoint/index.js
CHANGED
|
@@ -127,7 +127,6 @@ function buildApiServer(opts, lifecycle, dispatcherOverride, jobRunner, processL
|
|
|
127
127
|
metrics: opts.metrics,
|
|
128
128
|
extraRoutes: opts.extraRoutes,
|
|
129
129
|
observability: opts.observability,
|
|
130
|
-
observabilityOptions: opts.observabilityOptions,
|
|
131
130
|
dispatcherOptions,
|
|
132
131
|
systemHooks: opts.systemHooks,
|
|
133
132
|
eventDedup: opts.eventDedup,
|
|
@@ -147,7 +146,6 @@ function buildWorkerServer(opts, lifecycle, jobRunner) {
|
|
|
147
146
|
lifecycle,
|
|
148
147
|
processLane: "worker",
|
|
149
148
|
observability: opts.observability,
|
|
150
|
-
observabilityOptions: opts.observabilityOptions,
|
|
151
149
|
dispatcherOptions,
|
|
152
150
|
systemHooks: opts.systemHooks,
|
|
153
151
|
eventDedup: opts.eventDedup,
|
|
@@ -10,6 +10,19 @@ import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
|
|
|
10
10
|
// cannot be probed across tenants. `fileRefRows` holds only the writer's own
|
|
11
11
|
// tenant's live rows, so a missing entry is exactly those two cases. Values
|
|
12
12
|
// that are not UUIDs are skipped by fileRefIdsOf and never reach this check.
|
|
13
|
+
//
|
|
14
|
+
// Within the tenant, a newly named id must not be bound to a different record:
|
|
15
|
+
// the file routes authorize through the record a file is bound to, so naming
|
|
16
|
+
// record A's file in record B would hand B's readers A's file (and a later
|
|
17
|
+
// removal from B would not touch it). Pending uploads (no entityId), ids the
|
|
18
|
+
// record already holds in any file field (checked first, even if their row is gone)
|
|
19
|
+
// and moves between its own fields stay allowed. The error is the same as above so the owning record is not revealed.
|
|
20
|
+
function isBoundToOtherRecord(row, entityName, recordId) {
|
|
21
|
+
const boundId = row["entityId"];
|
|
22
|
+
if (boundId === null || boundId === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
return row["entityType"] !== entityName || boundId !== recordId;
|
|
25
|
+
}
|
|
13
26
|
export function assertFileRefsBelongToTenant(args) {
|
|
14
27
|
const { registry, result, fileRefRows } = args;
|
|
15
28
|
// skip: saves of unregistered aggregates (custom executors) declare no file fields
|
|
@@ -22,11 +35,24 @@ export function assertFileRefsBelongToTenant(args) {
|
|
|
22
35
|
// skip: without the files feature the file_refs table is not part of the schema
|
|
23
36
|
if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined)
|
|
24
37
|
return;
|
|
38
|
+
const recordId = String(result.id);
|
|
39
|
+
const alreadyHeld = new Set();
|
|
40
|
+
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
41
|
+
if (!isFileField(fieldDef))
|
|
42
|
+
continue;
|
|
43
|
+
for (const fileRefId of fileRefIdsOf(result.previous[fieldName]))
|
|
44
|
+
alreadyHeld.add(fileRefId);
|
|
45
|
+
}
|
|
25
46
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
26
47
|
if (!isFileField(fieldDef))
|
|
27
48
|
continue;
|
|
28
49
|
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
29
|
-
|
|
50
|
+
// A value the record already holds is no new binding; its file_refs row may be gone
|
|
51
|
+
// (forget purged the file), and sending the unchanged field back must still save.
|
|
52
|
+
if (alreadyHeld.has(fileRefId))
|
|
53
|
+
continue;
|
|
54
|
+
const row = fileRefRows.get(fileRefId);
|
|
55
|
+
if (row !== undefined && !isBoundToOtherRecord(row, result.entityName, recordId))
|
|
30
56
|
continue;
|
|
31
57
|
throw new ValidationError({
|
|
32
58
|
fields: [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TenantDb } from "../db/tenant-db.ts";
|
|
2
2
|
import { type Registry, type SaveContext, type SessionUser } from "../engine/types/index.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type FileRefRows } from "./load-save-file-refs.ts";
|
|
4
4
|
export declare function fileRefIdsOf(value: unknown): readonly string[];
|
|
5
5
|
export declare function bindPendingFileRefs(args: {
|
|
6
6
|
readonly registry: Registry;
|
|
@@ -9,3 +9,33 @@ export declare function bindPendingFileRefs(args: {
|
|
|
9
9
|
readonly db: TenantDb;
|
|
10
10
|
readonly fileRefRows: FileRefRows;
|
|
11
11
|
}): Promise<void>;
|
|
12
|
+
export type PendingFileRef = {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly storageKey: string;
|
|
15
|
+
readonly mimeType: string;
|
|
16
|
+
readonly size: number;
|
|
17
|
+
readonly version: number;
|
|
18
|
+
};
|
|
19
|
+
type PendingFileRefLookup = {
|
|
20
|
+
readonly fileRefId: string;
|
|
21
|
+
readonly entityName: string;
|
|
22
|
+
readonly fieldName: string;
|
|
23
|
+
readonly user: SessionUser;
|
|
24
|
+
readonly db: TenantDb;
|
|
25
|
+
};
|
|
26
|
+
export declare function loadPendingFileRef(lookup: PendingFileRefLookup): Promise<PendingFileRef | undefined>;
|
|
27
|
+
export type BindPendingFileRefOutcome = "bound" | "not-pending" | "version-conflict";
|
|
28
|
+
/**
|
|
29
|
+
* Binds one of the caller's own pending uploads to a record from a custom handler, with the
|
|
30
|
+
* same ownership predicate as the save pipeline. Anonymous callers never own an upload.
|
|
31
|
+
*
|
|
32
|
+
* The helper checks the upload, not the target: the caller must already be authorized to write
|
|
33
|
+
* `entityId` itself (for example a record the handler just wrote); `canWriteRecord` is not checked.
|
|
34
|
+
*
|
|
35
|
+
* A parallel bind of the same fileRef loses with "version-conflict" (or "not-pending" once the
|
|
36
|
+
* winner has committed); anything else throws.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bindPendingFileRef(args: PendingFileRefLookup & {
|
|
39
|
+
readonly entityId: string;
|
|
40
|
+
}): Promise<BindPendingFileRefOutcome>;
|
|
41
|
+
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { isAnonymousSessionUser } from "../engine/system-user.js";
|
|
1
2
|
import { isFileField, isUuid, } from "../engine/types/index.js";
|
|
2
3
|
import { reraiseAsKumikoError } from "../errors/index.js";
|
|
3
4
|
import { getFileRefExecutor } from "./create-file-ref.js";
|
|
4
5
|
import { FILE_REF_ENTITY_NAME } from "./file-ref-entity.js";
|
|
6
|
+
import { loadFileRefRows } from "./load-save-file-refs.js";
|
|
5
7
|
export function fileRefIdsOf(value) {
|
|
6
8
|
if (typeof value === "string")
|
|
7
9
|
return isUuid(value) ? [value] : [];
|
|
@@ -15,6 +17,16 @@ function isPendingUploadFor(row, entityName, fieldName, user) {
|
|
|
15
17
|
row["fieldName"] === fieldName &&
|
|
16
18
|
row["insertedById"] === user.id);
|
|
17
19
|
}
|
|
20
|
+
function streamVersionOf(row) {
|
|
21
|
+
return typeof row["version"] === "number" ? row["version"] : undefined;
|
|
22
|
+
}
|
|
23
|
+
async function bindPendingRow(args) {
|
|
24
|
+
const { row, fileRefId, entityName, fieldName, entityId, user, db } = args;
|
|
25
|
+
if (!isPendingUploadFor(row, entityName, fieldName, user))
|
|
26
|
+
return { kind: "not-pending" };
|
|
27
|
+
const bound = await getFileRefExecutor().update({ id: fileRefId, version: streamVersionOf(row), changes: { entityId } }, user, db);
|
|
28
|
+
return bound.isSuccess ? { kind: "bound" } : { kind: "failed", error: bound.error };
|
|
29
|
+
}
|
|
18
30
|
// A form (or a server handler via createFileRefFromBytes) uploads before the
|
|
19
31
|
// record references the file, so its fileRefs carry entityType + fieldName but
|
|
20
32
|
// no entityId. Binding them in the save's own transaction makes record-bound
|
|
@@ -35,7 +47,6 @@ export async function bindPendingFileRefs(args) {
|
|
|
35
47
|
// skip: without the files feature the file_refs table is not part of the schema
|
|
36
48
|
if (registry.getEntity(FILE_REF_ENTITY_NAME) === undefined)
|
|
37
49
|
return;
|
|
38
|
-
const executor = getFileRefExecutor();
|
|
39
50
|
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
40
51
|
if (!isFileField(fieldDef))
|
|
41
52
|
continue;
|
|
@@ -43,12 +54,75 @@ export async function bindPendingFileRefs(args) {
|
|
|
43
54
|
// ones bound earlier; only the ids this save names can be pending uploads.
|
|
44
55
|
for (const fileRefId of fileRefIdsOf(result.changes[fieldName])) {
|
|
45
56
|
const row = fileRefRows.get(fileRefId);
|
|
46
|
-
if (row === undefined
|
|
57
|
+
if (row === undefined)
|
|
47
58
|
continue;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
const outcome = await bindPendingRow({
|
|
60
|
+
row,
|
|
61
|
+
fileRefId,
|
|
62
|
+
entityName,
|
|
63
|
+
fieldName,
|
|
64
|
+
entityId: String(result.id),
|
|
65
|
+
user,
|
|
66
|
+
db,
|
|
67
|
+
});
|
|
68
|
+
if (outcome.kind === "failed")
|
|
69
|
+
throw reraiseAsKumikoError(outcome.error);
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
}
|
|
73
|
+
async function loadPendingRow(lookup) {
|
|
74
|
+
const { fileRefId, entityName, fieldName, user, db } = lookup;
|
|
75
|
+
// Every anonymous visitor shares one user id, so insertedById cannot tell their uploads apart.
|
|
76
|
+
if (isAnonymousSessionUser(user))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (!isUuid(fileRefId))
|
|
79
|
+
return undefined;
|
|
80
|
+
const row = (await loadFileRefRows(db, [fileRefId], user.tenantId)).get(fileRefId);
|
|
81
|
+
return row !== undefined && isPendingUploadFor(row, entityName, fieldName, user)
|
|
82
|
+
? row
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
85
|
+
// fileName is left out on purpose: it is personal data and may be encrypted; read it through
|
|
86
|
+
// the fileRef executor's detail when it is needed.
|
|
87
|
+
export async function loadPendingFileRef(lookup) {
|
|
88
|
+
const row = await loadPendingRow(lookup);
|
|
89
|
+
if (row === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
const { storageKey, mimeType, size } = row;
|
|
92
|
+
const version = streamVersionOf(row);
|
|
93
|
+
if (typeof storageKey !== "string" ||
|
|
94
|
+
typeof mimeType !== "string" ||
|
|
95
|
+
typeof size !== "number" ||
|
|
96
|
+
version === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return { id: lookup.fileRefId, storageKey, mimeType, size, version };
|
|
100
|
+
}
|
|
101
|
+
const CONCURRENT_BIND_ERROR_CODES = ["version_conflict", "unique_violation"];
|
|
102
|
+
/**
|
|
103
|
+
* Binds one of the caller's own pending uploads to a record from a custom handler, with the
|
|
104
|
+
* same ownership predicate as the save pipeline. Anonymous callers never own an upload.
|
|
105
|
+
*
|
|
106
|
+
* The helper checks the upload, not the target: the caller must already be authorized to write
|
|
107
|
+
* `entityId` itself (for example a record the handler just wrote); `canWriteRecord` is not checked.
|
|
108
|
+
*
|
|
109
|
+
* A parallel bind of the same fileRef loses with "version-conflict" (or "not-pending" once the
|
|
110
|
+
* winner has committed); anything else throws.
|
|
111
|
+
*/
|
|
112
|
+
export async function bindPendingFileRef(args) {
|
|
113
|
+
const row = await loadPendingRow(args);
|
|
114
|
+
// Without a stream version the update would run unversioned and could overwrite a concurrent bind.
|
|
115
|
+
if (row === undefined || streamVersionOf(row) === undefined)
|
|
116
|
+
return "not-pending";
|
|
117
|
+
const outcome = await bindPendingRow({ ...args, row });
|
|
118
|
+
switch (outcome.kind) {
|
|
119
|
+
case "bound":
|
|
120
|
+
return "bound";
|
|
121
|
+
case "not-pending":
|
|
122
|
+
return "not-pending";
|
|
123
|
+
case "failed":
|
|
124
|
+
if (CONCURRENT_BIND_ERROR_CODES.includes(outcome.error.code))
|
|
125
|
+
return "version-conflict";
|
|
126
|
+
throw reraiseAsKumikoError(outcome.error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fetchOne } from "../db/query.js";
|
|
1
|
+
import { fetchOne, runnerCanBeginTransaction } from "../db/query.js";
|
|
2
|
+
import { tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
2
3
|
import { FrameworkReasons, InternalError, UnprocessableError, writeFailure, } from "../errors/index.js";
|
|
3
4
|
import { createFileRefRow, DEFAULT_MAX_UPLOAD_SIZE, storeFileAsFileRef, writeAcceptedFileBytes, } from "./create-file-ref.js";
|
|
4
5
|
import { fileRefsTable } from "./file-ref-table.js";
|
|
@@ -22,9 +23,10 @@ export async function createFileRefFromBytes(ctx, input) {
|
|
|
22
23
|
const outcome = await storeFileAsFileRef({
|
|
23
24
|
user: ctx.user,
|
|
24
25
|
tenantDb: ctx.db,
|
|
26
|
+
rowInOwnTransaction: runnerCanBeginTransaction(tenantDbRunner(ctx.db)),
|
|
25
27
|
registry: ctx.registry,
|
|
26
28
|
defaultMaxUploadSize: readFilesRouteOptions(ctx.registry).maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
|
|
27
|
-
writeBytes: (key, data, mimeType) => files.
|
|
29
|
+
writeBytes: (key, data, mimeType) => files.writeUploadBytes(key, data, mimeType),
|
|
28
30
|
deleteBytes: (key) => files.ref(key).delete(),
|
|
29
31
|
}, {
|
|
30
32
|
fileName: input.fileName,
|
|
@@ -48,7 +50,8 @@ function fileRejectedFailure(error) {
|
|
|
48
50
|
}
|
|
49
51
|
// Pre-transaction half of createFileRefFromBytes: validates and writes the binary while no
|
|
50
52
|
// transaction holds a connection. Pair it with createFileRefFromUploadedBytes inside the handler.
|
|
51
|
-
// A crash between this phase and the commit leaves the bytes in storage;
|
|
53
|
+
// A crash between this phase and the commit leaves the bytes in storage; the upload ledger sweep
|
|
54
|
+
// removes them after its grace period.
|
|
52
55
|
export async function uploadFileBytesBeforeTransaction(ctx, input) {
|
|
53
56
|
const { files } = ctx;
|
|
54
57
|
if (!files) {
|
|
@@ -60,7 +63,7 @@ export async function uploadFileBytesBeforeTransaction(ctx, input) {
|
|
|
60
63
|
user: ctx.user,
|
|
61
64
|
registry: ctx.registry,
|
|
62
65
|
defaultMaxUploadSize: readFilesRouteOptions(ctx.registry).maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE,
|
|
63
|
-
writeBytes: (key, data, mimeType) => files.
|
|
66
|
+
writeBytes: (key, data, mimeType) => files.writeUploadBytes(key, data, mimeType),
|
|
64
67
|
}, {
|
|
65
68
|
fileName: input.fileName,
|
|
66
69
|
declaredMimeType: input.mimeType,
|
|
@@ -80,8 +83,11 @@ export async function uploadFileBytesBeforeTransaction(ctx, input) {
|
|
|
80
83
|
const committedRow = await fetchOne(ctx.db, fileRefsTable, {
|
|
81
84
|
storageKey: uploaded.storageKey,
|
|
82
85
|
});
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
// skip: a committed row owns the bytes, the commit already took the ledger entry
|
|
87
|
+
if (committedRow)
|
|
88
|
+
return;
|
|
89
|
+
await files.ref(uploaded.storageKey).delete();
|
|
90
|
+
await files.forgetUploadBytes(uploaded.storageKey);
|
|
85
91
|
},
|
|
86
92
|
};
|
|
87
93
|
}
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { type TenantDb } from "../db/tenant-db.ts";
|
|
2
2
|
import { type AnyFileFieldDef, type Registry, type SessionUser } from "../engine/types/index.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type WriteFailure } from "../errors/index.ts";
|
|
4
4
|
export declare const DEFAULT_MAX_UPLOAD_SIZE = "10mb";
|
|
5
|
-
declare function buildFileRefExecutor(): import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor
|
|
5
|
+
declare function buildFileRefExecutor(): import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<{
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly tenantId: string;
|
|
8
|
+
readonly version: number;
|
|
9
|
+
readonly insertedAt: Temporal.Instant;
|
|
10
|
+
readonly modifiedAt: Temporal.Instant | null;
|
|
11
|
+
readonly insertedById: string | null;
|
|
12
|
+
readonly modifiedById: string | null;
|
|
13
|
+
readonly isDeleted?: boolean | undefined;
|
|
14
|
+
readonly deletedAt?: Temporal.Instant | null | undefined;
|
|
15
|
+
readonly deletedById?: string | null | undefined;
|
|
16
|
+
readonly entityId: string | null;
|
|
17
|
+
readonly entityType: string | null;
|
|
18
|
+
readonly fieldName: string | null;
|
|
19
|
+
readonly fileName: string;
|
|
20
|
+
readonly mimeType: string;
|
|
21
|
+
readonly size: unknown;
|
|
22
|
+
readonly storageKey: string;
|
|
23
|
+
}>;
|
|
6
24
|
export declare function getFileRefExecutor(): ReturnType<typeof buildFileRefExecutor>;
|
|
7
25
|
type AttachedFieldResolution = {
|
|
8
26
|
readonly kind: "unattached";
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createEventStoreExecutor } from "../db/event-store-executor.js";
|
|
2
2
|
import { runInOwnTransaction } from "../db/tenant-db.js";
|
|
3
3
|
import { isFileField, } from "../engine/types/index.js";
|
|
4
|
+
import { InternalError } from "../errors/index.js";
|
|
4
5
|
import { generateId } from "../utils/index.js";
|
|
5
6
|
import { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.js";
|
|
6
7
|
import { fileRefsTable } from "./file-ref-table.js";
|
|
7
8
|
import { resolveContentType } from "./resolve-content-type.js";
|
|
8
9
|
import { buildStorageKey, resolveUploadMimeType, validateFile, validateFileContent, } from "./types.js";
|
|
10
|
+
import { takeUploadLedgerEntry } from "./upload-ledger.js";
|
|
9
11
|
export const DEFAULT_MAX_UPLOAD_SIZE = "10mb";
|
|
10
12
|
// Standard entity executor for fileRef: create/delete emit fileRef.created /
|
|
11
13
|
// fileRef.deleted and write file_refs via applyEntityEvent in one tx
|
|
@@ -132,6 +134,13 @@ export async function createFileRefRow(deps, uploaded) {
|
|
|
132
134
|
}, deps.user, deps.tenantDb);
|
|
133
135
|
if (!result.isSuccess)
|
|
134
136
|
return { failure: result };
|
|
137
|
+
// Throws instead of returning a failure: a returned failure would still commit in the
|
|
138
|
+
// own-transaction path, leaving a fileRef row whose bytes the sweep already deleted.
|
|
139
|
+
if (!(await takeUploadLedgerEntry(deps.tenantDb, uploaded.storageKey))) {
|
|
140
|
+
throw new InternalError({
|
|
141
|
+
message: "upload expired: the bytes were swept before the fileRef row was created, upload again",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
135
144
|
return {
|
|
136
145
|
fileRef: {
|
|
137
146
|
id: fileRefId,
|
|
@@ -148,7 +157,7 @@ export async function storeFileAsFileRef(deps, input) {
|
|
|
148
157
|
if (upload.kind === "rejected")
|
|
149
158
|
return upload;
|
|
150
159
|
// A failed create deletes the bytes again; a later rollback of an enclosing transaction or a
|
|
151
|
-
// crash cannot, so
|
|
160
|
+
// crash cannot, so the upload ledger sweep removes those bytes after its grace period.
|
|
152
161
|
// Losing a row on append-failure is acceptable; a committed row with a missing binary is not.
|
|
153
162
|
// A throwing projection on fileRef.created fails the create by exception, not by WriteFailure.
|
|
154
163
|
const deleteUploadedBytes = async () => {
|
package/dist/files/feature.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type FilesFeatureOptions = {
|
|
|
6
6
|
readonly privilegedRoles?: readonly string[];
|
|
7
7
|
readonly unclaimedUploadReaders?: readonly string[];
|
|
8
8
|
readonly maxUploadSize?: string;
|
|
9
|
+
readonly uploadSweepGraceHours?: number;
|
|
9
10
|
};
|
|
10
11
|
export declare const FILES_FEATURE: "files";
|
|
11
12
|
export declare function createFilesFeature(options?: FilesFeatureOptions): FeatureDefinition;
|
package/dist/files/feature.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineFeature } from "../engine/index.js";
|
|
2
2
|
import { bindReferencedFilesWrite } from "./bind-referenced-files.write.js";
|
|
3
3
|
import { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.js";
|
|
4
|
+
import { DEFAULT_UPLOAD_SWEEP_GRACE_HOURS, MIN_UPLOAD_SWEEP_GRACE_HOURS, sweepOrphanedUploads, } from "./sweep-orphaned-uploads.js";
|
|
5
|
+
import { fileUploadLedgerTable } from "./upload-ledger.js";
|
|
4
6
|
export { fileRefEntity } from "./file-ref-entity.js";
|
|
5
7
|
// files — `fileRef` als ganz normales ES-Entity. Upload/Delete laufen über
|
|
6
8
|
// den Standard-Entity-Executor (file-routes.ts: executor.create/delete →
|
|
@@ -14,7 +16,14 @@ export { fileRefEntity } from "./file-ref-entity.js";
|
|
|
14
16
|
// Entity-Projektion (für rebuildProjection). Liegt im Framework neben
|
|
15
17
|
// file-routes + fileRefsTable; bundled-features/files re-exportiert nur.
|
|
16
18
|
export const FILES_FEATURE = "files";
|
|
19
|
+
function assertValidSweepGraceHours(hours) {
|
|
20
|
+
if (!Number.isInteger(hours) || hours < MIN_UPLOAD_SWEEP_GRACE_HOURS) {
|
|
21
|
+
throw new Error(`createFilesFeature: uploadSweepGraceHours must be a whole number of hours >= ${MIN_UPLOAD_SWEEP_GRACE_HOURS}, got ${String(hours)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
export function createFilesFeature(options = {}) {
|
|
25
|
+
const uploadSweepGraceHours = options.uploadSweepGraceHours ?? DEFAULT_UPLOAD_SWEEP_GRACE_HOURS;
|
|
26
|
+
assertValidSweepGraceHours(uploadSweepGraceHours);
|
|
18
27
|
return defineFeature(FILES_FEATURE, (r) => {
|
|
19
28
|
r.describe("Exposes the `fileRef` entity from the framework core so that uploaded files — tracked in the `file_refs` table by `createFileRoutes` — participate in cross-feature hooks: `user-data-rights-defaults` automatically includes file blobs in GDPR exports and forget flows, and tenant-lifecycle cleanup deletes all refs on tenant destroy. Upload/download routes are registered by the server bootstrap when a `file-provider-*` feature is mounted; this feature carries their access/size policy via `createFilesFeature(opts?)`.");
|
|
20
29
|
r.uiHints({
|
|
@@ -24,6 +33,22 @@ export function createFilesFeature(options = {}) {
|
|
|
24
33
|
});
|
|
25
34
|
r.entity({ name: FILE_REF_ENTITY_NAME, ...fileRefEntity });
|
|
26
35
|
r.writeHandler(bindReferencedFilesWrite);
|
|
36
|
+
r.storeTable(fileUploadLedgerTable, { reason: "coordination.file_upload_ledger" });
|
|
37
|
+
r.job({
|
|
38
|
+
name: "sweep-orphaned-uploads",
|
|
39
|
+
trigger: { cron: "0 * * * *" },
|
|
40
|
+
perTenant: true,
|
|
41
|
+
concurrency: "skip",
|
|
42
|
+
handler: async (_payload, ctx) => {
|
|
43
|
+
await sweepOrphanedUploads({
|
|
44
|
+
db: ctx.db,
|
|
45
|
+
files: ctx.files,
|
|
46
|
+
now: ctx.tz.now(),
|
|
47
|
+
graceHours: uploadSweepGraceHours,
|
|
48
|
+
log: ctx.log,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|
|
27
52
|
return { routeOptions: options };
|
|
28
53
|
});
|
|
29
54
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { FileContext, FileHandle } from "@cosmicdrift/kumiko-types/file-handle-types";
|
|
2
|
+
import type { FileProviderResolver, FileUploadLedger } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
3
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
2
4
|
import { type FileStorageProvider } from "./types.ts";
|
|
3
5
|
export type { FileContext, FileHandle };
|
|
4
6
|
export declare function createFileHandle(key: string, getProvider: () => Promise<FileStorageProvider>): FileHandle;
|
|
5
|
-
export declare function createFileContext(resolve: () => Promise<FileStorageProvider>, tenantId?: string): FileContext;
|
|
7
|
+
export declare function createFileContext(resolve: () => Promise<FileStorageProvider>, tenantId?: string, uploadLedger?: FileUploadLedger): FileContext;
|
|
8
|
+
export declare function createTenantFileContext(resolver: FileProviderResolver, tenantId: TenantId): FileContext;
|
|
6
9
|
export declare function deriveKey(key: string, suffix: string): string;
|
|
7
10
|
export declare function storageKeyStemPrefix(key: string): string;
|
|
@@ -11,6 +11,7 @@
|
|
|
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
13
|
import { isDerivativeSuffix } from "../derivatives/variant-key.js";
|
|
14
|
+
import { InternalError } from "../errors/index.js";
|
|
14
15
|
import { assertSafeStorageKey } from "./types.js";
|
|
15
16
|
// `getProvider` is a lazily-resolved, memoized accessor — the provider is
|
|
16
17
|
// resolved (config + s3.secretAccessKey secret read) only when a handle method
|
|
@@ -42,19 +43,39 @@ export function createFileHandle(key, getProvider) {
|
|
|
42
43
|
// With `tenantId`, `ref` and `list` only accept keys/prefixes inside `${tenantId}/` —
|
|
43
44
|
// the key space is the isolation boundary in a store shared by several tenants, and
|
|
44
45
|
// a request-derived key or prefix (or "") would otherwise reach every tenant's keys.
|
|
45
|
-
export function createFileContext(resolve, tenantId) {
|
|
46
|
+
export function createFileContext(resolve, tenantId, uploadLedger) {
|
|
46
47
|
let cached;
|
|
47
48
|
const getProvider = () => {
|
|
48
49
|
cached ??= resolve();
|
|
49
50
|
return cached;
|
|
50
51
|
};
|
|
52
|
+
const requireUploadLedger = () => {
|
|
53
|
+
if (uploadLedger === undefined) {
|
|
54
|
+
throw new InternalError({
|
|
55
|
+
message: "FileContext: no upload ledger is wired, so upload bytes would be unrecoverable after a crash. Build the context from a resolver made by withFileProviderResolver (or withUploadLedger)",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return uploadLedger;
|
|
59
|
+
};
|
|
60
|
+
const assertOwnKey = (method, key) => {
|
|
61
|
+
if (tenantId !== undefined && !key.startsWith(`${tenantId}/`)) {
|
|
62
|
+
throw new Error(`FileContext.${method}: key must start with "${tenantId}/"`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
51
65
|
return {
|
|
52
66
|
ref: (key) => {
|
|
53
|
-
|
|
54
|
-
throw new Error(`FileContext.ref: key must start with "${tenantId}/"`);
|
|
55
|
-
}
|
|
67
|
+
assertOwnKey("ref", key);
|
|
56
68
|
return createFileHandle(key, getProvider);
|
|
57
69
|
},
|
|
70
|
+
writeUploadBytes: async (key, data, mimeType) => {
|
|
71
|
+
assertOwnKey("writeUploadBytes", key);
|
|
72
|
+
await requireUploadLedger().record(key);
|
|
73
|
+
await (await getProvider()).write(key, data, mimeType);
|
|
74
|
+
},
|
|
75
|
+
forgetUploadBytes: async (key) => {
|
|
76
|
+
assertOwnKey("forgetUploadBytes", key);
|
|
77
|
+
await requireUploadLedger().forget(key);
|
|
78
|
+
},
|
|
58
79
|
/** Validates only the storage key shape (assertSafeStorageKey), no tenant prefix: keys
|
|
59
80
|
* may outlive a tenant handover. The caller must ensure the fileRef row came from a
|
|
60
81
|
* tenant-filtered read. */
|
|
@@ -74,6 +95,11 @@ export function createFileContext(resolve, tenantId) {
|
|
|
74
95
|
},
|
|
75
96
|
};
|
|
76
97
|
}
|
|
98
|
+
// The one way to build a tenant's FileContext from a resolver: carries the resolver's upload
|
|
99
|
+
// ledger, so every context agrees on where upload bytes are tracked.
|
|
100
|
+
export function createTenantFileContext(resolver, tenantId) {
|
|
101
|
+
return createFileContext(() => resolver(tenantId), tenantId, resolver.uploadLedgerFor?.(tenantId));
|
|
102
|
+
}
|
|
77
103
|
// Splits a key into the part before the last dot of its last path segment
|
|
78
104
|
// (the "stem") and the extension including the dot. No dot in the last
|
|
79
105
|
// segment: the whole key is the stem, extension is empty.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { FILE_REF_ENTITY_NAME } from "./types.ts";
|
|
2
2
|
export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
|
|
3
|
-
storageKey: {
|
|
3
|
+
readonly storageKey: {
|
|
4
4
|
readonly type: "text";
|
|
5
5
|
readonly description?: string;
|
|
6
6
|
readonly maxLength?: number;
|
|
@@ -23,7 +23,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
23
23
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
24
24
|
required: true;
|
|
25
25
|
};
|
|
26
|
-
fileName: {
|
|
26
|
+
readonly fileName: {
|
|
27
27
|
readonly type: "text";
|
|
28
28
|
readonly description?: string;
|
|
29
29
|
readonly maxLength?: number;
|
|
@@ -46,7 +46,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
46
46
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
47
47
|
required: true;
|
|
48
48
|
};
|
|
49
|
-
mimeType: {
|
|
49
|
+
readonly mimeType: {
|
|
50
50
|
readonly type: "text";
|
|
51
51
|
readonly description?: string;
|
|
52
52
|
readonly maxLength?: number;
|
|
@@ -69,7 +69,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
69
69
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
70
70
|
required: true;
|
|
71
71
|
};
|
|
72
|
-
size: {
|
|
72
|
+
readonly size: {
|
|
73
73
|
readonly type: "bigInt";
|
|
74
74
|
readonly description?: string;
|
|
75
75
|
readonly required?: boolean;
|
|
@@ -81,7 +81,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
81
81
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
82
82
|
required: true;
|
|
83
83
|
};
|
|
84
|
-
entityType: {
|
|
84
|
+
readonly entityType: {
|
|
85
85
|
readonly type: "text";
|
|
86
86
|
readonly description?: string;
|
|
87
87
|
readonly maxLength?: number;
|
|
@@ -104,7 +104,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
104
104
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
105
105
|
required: false;
|
|
106
106
|
};
|
|
107
|
-
entityId: {
|
|
107
|
+
readonly entityId: {
|
|
108
108
|
readonly type: "text";
|
|
109
109
|
readonly description?: string;
|
|
110
110
|
readonly maxLength?: number;
|
|
@@ -127,7 +127,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
127
127
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
128
128
|
required: false;
|
|
129
129
|
};
|
|
130
|
-
fieldName: {
|
|
130
|
+
readonly fieldName: {
|
|
131
131
|
readonly type: "text";
|
|
132
132
|
readonly description?: string;
|
|
133
133
|
readonly maxLength?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable<import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
|
|
2
|
-
storageKey: {
|
|
2
|
+
readonly storageKey: {
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
@@ -22,7 +22,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
22
22
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
23
23
|
required: true;
|
|
24
24
|
};
|
|
25
|
-
fileName: {
|
|
25
|
+
readonly fileName: {
|
|
26
26
|
readonly type: "text";
|
|
27
27
|
readonly description?: string;
|
|
28
28
|
readonly maxLength?: number;
|
|
@@ -45,7 +45,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
45
45
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
46
46
|
required: true;
|
|
47
47
|
};
|
|
48
|
-
mimeType: {
|
|
48
|
+
readonly mimeType: {
|
|
49
49
|
readonly type: "text";
|
|
50
50
|
readonly description?: string;
|
|
51
51
|
readonly maxLength?: number;
|
|
@@ -68,7 +68,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
68
68
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
69
69
|
required: true;
|
|
70
70
|
};
|
|
71
|
-
size: {
|
|
71
|
+
readonly size: {
|
|
72
72
|
readonly type: "bigInt";
|
|
73
73
|
readonly description?: string;
|
|
74
74
|
readonly required?: boolean;
|
|
@@ -80,7 +80,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
80
80
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
81
81
|
required: true;
|
|
82
82
|
};
|
|
83
|
-
entityType: {
|
|
83
|
+
readonly entityType: {
|
|
84
84
|
readonly type: "text";
|
|
85
85
|
readonly description?: string;
|
|
86
86
|
readonly maxLength?: number;
|
|
@@ -103,7 +103,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
103
103
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
104
104
|
required: false;
|
|
105
105
|
};
|
|
106
|
-
entityId: {
|
|
106
|
+
readonly entityId: {
|
|
107
107
|
readonly type: "text";
|
|
108
108
|
readonly description?: string;
|
|
109
109
|
readonly maxLength?: number;
|
|
@@ -126,7 +126,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
126
126
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
127
127
|
required: false;
|
|
128
128
|
};
|
|
129
|
-
fieldName: {
|
|
129
|
+
readonly fieldName: {
|
|
130
130
|
readonly type: "text";
|
|
131
131
|
readonly description?: string;
|
|
132
132
|
readonly maxLength?: number;
|