@cosmicdrift/kumiko-framework 0.383.0 → 0.384.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/examples/custom-fields-basic.md +13 -3
- package/agents/examples/designer-demo.md +6 -2
- package/dist/db/connection.d.ts +5 -1
- package/dist/db/connection.js +29 -9
- package/dist/db/query.js +29 -1
- package/dist/db/statement-timeout.js +2 -2
- package/dist/engine/boot-validator/i18n-keys.d.ts +1 -0
- package/dist/engine/boot-validator/i18n-keys.js +8 -1
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/pii-retention.js +6 -7
- package/dist/engine/build-app-schema.js +1 -1
- package/dist/engine/factories.d.ts +3 -3
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/files/file-ref-entity.d.ts +6 -126
- package/dist/files/file-ref-table.d.ts +6 -126
- package/dist/files/file-routes.js +38 -15
- package/dist/i18n/required-surface-keys.d.ts +6 -2
- package/dist/i18n/required-surface-keys.js +29 -3
- package/dist/pipeline/event-dispatcher-admin.d.ts +15 -8
- package/dist/pipeline/event-dispatcher-admin.js +28 -7
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +1 -1
- package/dist/pipeline/index.d.ts +2 -2
- package/dist/pipeline/index.js +1 -1
- package/package.json +3 -3
- package/src/changes.json +32 -0
|
@@ -239,7 +239,7 @@ import {
|
|
|
239
239
|
wireCustomFieldsFor,
|
|
240
240
|
} from "@cosmicdrift/kumiko-bundled-features/custom-fields";
|
|
241
241
|
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
242
|
-
import { createEntity, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
242
|
+
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
243
243
|
import { z } from "zod";
|
|
244
244
|
|
|
245
245
|
// --- Entity ---
|
|
@@ -254,7 +254,12 @@ import { z } from "zod";
|
|
|
254
254
|
export const propertyEntity = createEntity({
|
|
255
255
|
table: "read_sample_cf_properties",
|
|
256
256
|
fields: {
|
|
257
|
-
name: {
|
|
257
|
+
name: createTextField({
|
|
258
|
+
required: true,
|
|
259
|
+
maxLength: 200,
|
|
260
|
+
personal: false,
|
|
261
|
+
reason: "property_name_is_business_data",
|
|
262
|
+
}),
|
|
258
263
|
customFields: customFieldsField(),
|
|
259
264
|
},
|
|
260
265
|
});
|
|
@@ -274,7 +279,12 @@ export const propertyFeature = defineFeature("property-management", (r) => {
|
|
|
274
279
|
name: "property",
|
|
275
280
|
table: "read_sample_cf_properties",
|
|
276
281
|
fields: {
|
|
277
|
-
name: {
|
|
282
|
+
name: createTextField({
|
|
283
|
+
required: true,
|
|
284
|
+
maxLength: 200,
|
|
285
|
+
personal: false,
|
|
286
|
+
reason: "property_name_is_business_data",
|
|
287
|
+
}),
|
|
278
288
|
customFields: { type: "jsonb" },
|
|
279
289
|
},
|
|
280
290
|
});
|
|
@@ -36,7 +36,7 @@ No dedicated automated tests in this folder yet — use it as a composition samp
|
|
|
36
36
|
// - nav → ID/Label/Screen-Card
|
|
37
37
|
// Plus optionalRequires + metric als JSON-Dump-Fallback.
|
|
38
38
|
|
|
39
|
-
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
39
|
+
import { createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
40
40
|
import { z } from "zod";
|
|
41
41
|
|
|
42
42
|
defineFeature("designerDemo", (r) => {
|
|
@@ -45,7 +45,11 @@ defineFeature("designerDemo", (r) => {
|
|
|
45
45
|
r.entity({
|
|
46
46
|
name: "task",
|
|
47
47
|
fields: {
|
|
48
|
-
title: {
|
|
48
|
+
title: createTextField({
|
|
49
|
+
required: true,
|
|
50
|
+
personal: false,
|
|
51
|
+
reason: "demo_task_title_is_not_personal_data",
|
|
52
|
+
}),
|
|
49
53
|
done: { type: "boolean", default: false },
|
|
50
54
|
priority: { type: "select", options: ["low", "medium", "high"], default: "medium" },
|
|
51
55
|
},
|
package/dist/db/connection.d.ts
CHANGED
|
@@ -22,7 +22,11 @@ export type DbConnectionHandle = {
|
|
|
22
22
|
close: () => Promise<void>;
|
|
23
23
|
};
|
|
24
24
|
export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
|
|
25
|
-
export
|
|
25
|
+
export type UnboundedSiblingPool = {
|
|
26
|
+
readonly client: PgClient;
|
|
27
|
+
readonly close: (timeoutSeconds: number) => Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
export declare function openUnboundedSiblingPool(db: unknown): UnboundedSiblingPool | undefined;
|
|
26
30
|
export declare function createDbConnection({ url, ...options }: CreateDbConnectionOptions): DbConnectionHandle;
|
|
27
31
|
export declare function dbConnectionOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): DbConnectionOptions;
|
|
28
32
|
export declare const DEFAULT_SERVER_STATEMENT_TIMEOUT_MS = 60000;
|
package/dist/db/connection.js
CHANGED
|
@@ -38,18 +38,38 @@ export function createDbConnection({ url, ...options }) {
|
|
|
38
38
|
if (Object.keys(serverParameters).length > 0)
|
|
39
39
|
pgOptions.connection = serverParameters;
|
|
40
40
|
const client = postgres(url, pgOptions);
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
// A run that never settles (a caller gave up on it via its own timeout) never reaches the
|
|
42
|
+
// sibling's own close; close() ends it so shutdown and DROP DATABASE are not blocked by it.
|
|
43
|
+
const openSiblings = new Set();
|
|
44
|
+
unboundedSiblingFactories.set(client, () => {
|
|
45
|
+
// Two connections are enough for lock-in-tx plus DDL; the cap keeps parallel rebuilds from exhausting max_connections.
|
|
46
|
+
const sibling = postgres(url, {
|
|
47
|
+
...pgOptions,
|
|
48
|
+
max: 4,
|
|
49
|
+
connection: { ...pgOptions.connection, statement_timeout: 0 },
|
|
50
|
+
});
|
|
51
|
+
openSiblings.add(sibling);
|
|
52
|
+
return {
|
|
53
|
+
client: sibling,
|
|
54
|
+
close: async (timeoutSeconds) => {
|
|
55
|
+
openSiblings.delete(sibling);
|
|
56
|
+
await sibling.end({ timeout: timeoutSeconds });
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
47
60
|
return {
|
|
48
61
|
db: client,
|
|
49
62
|
client,
|
|
50
|
-
close: () =>
|
|
51
|
-
timeout
|
|
52
|
-
|
|
63
|
+
close: async () => {
|
|
64
|
+
const timeout = options.closeTimeoutSeconds ?? DEFAULT_DB_CLOSE_TIMEOUT_SECONDS;
|
|
65
|
+
unboundedSiblingFactories.delete(client);
|
|
66
|
+
const pools = [client, ...openSiblings];
|
|
67
|
+
openSiblings.clear();
|
|
68
|
+
const results = await Promise.allSettled(pools.map((pool) => pool.end({ timeout })));
|
|
69
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
70
|
+
if (failure !== undefined)
|
|
71
|
+
throw failure.reason;
|
|
72
|
+
},
|
|
53
73
|
};
|
|
54
74
|
}
|
|
55
75
|
// DB_PROVIDER=bun selected the removed Bun.SQL driver; ignoring it would boot
|
package/dist/db/query.js
CHANGED
|
@@ -147,7 +147,17 @@ function assertNotTenantScoped(db, fnName) {
|
|
|
147
147
|
"explicit cross-tenant op, or use a scoped TenantDb method.");
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
const WHERE_OPERATOR_KEYS = [
|
|
150
|
+
const WHERE_OPERATOR_KEYS = [
|
|
151
|
+
"gt",
|
|
152
|
+
"gte",
|
|
153
|
+
"lt",
|
|
154
|
+
"lte",
|
|
155
|
+
"ne",
|
|
156
|
+
"in",
|
|
157
|
+
"notIn",
|
|
158
|
+
"like",
|
|
159
|
+
"jsonText",
|
|
160
|
+
];
|
|
151
161
|
const _whereOperatorKeysExhaustive = true;
|
|
152
162
|
void _whereOperatorKeysExhaustive;
|
|
153
163
|
function isJsonTextMatchList(value) {
|
|
@@ -664,6 +674,24 @@ function buildWhereClause(info, where, startIndex, qualifier) {
|
|
|
664
674
|
conditions.push(`${ref(col)} IN (${parts.join(", ")})`);
|
|
665
675
|
}
|
|
666
676
|
}
|
|
677
|
+
const notInVal = value.notIn;
|
|
678
|
+
if (Array.isArray(notInVal)) {
|
|
679
|
+
if (pgType === "jsonb") {
|
|
680
|
+
throw new Error(`where: notIn is not supported on jsonb column "${col}"`);
|
|
681
|
+
}
|
|
682
|
+
const parts = [];
|
|
683
|
+
for (const v of notInVal) {
|
|
684
|
+
const p = prepareValue(v, pgType);
|
|
685
|
+
if (p.kind === "literal") {
|
|
686
|
+
parts.push(p.literal);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
parts.push(`$${idx++}${p.sql}`);
|
|
690
|
+
values.push(p.bound);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
conditions.push(parts.length === 0 ? "TRUE" : `${ref(col)} NOT IN (${parts.join(", ")})`);
|
|
694
|
+
}
|
|
667
695
|
}
|
|
668
696
|
else if (pgType === "jsonb" && isJsonbScalar(value)) {
|
|
669
697
|
conditions.push(jsonbContainsAny(col, [value]));
|
|
@@ -10,9 +10,9 @@ export async function withoutStatementTimeout(db, run) {
|
|
|
10
10
|
if (unbounded === undefined)
|
|
11
11
|
return run(db);
|
|
12
12
|
try {
|
|
13
|
-
return await run(unbounded);
|
|
13
|
+
return await run(unbounded.client);
|
|
14
14
|
}
|
|
15
15
|
finally {
|
|
16
|
-
await unbounded.
|
|
16
|
+
await unbounded.close(UNBOUNDED_POOL_CLOSE_TIMEOUT_SECONDS);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildEffectiveTranslationKeys, featureHasI18nSurface, requiredKeysFromFeature, requiredKeysFromNav, requiredKeysFromScreen, requiredKeysFromWorkspace, screenTitleKey, } from "../../i18n/required-surface-keys.js";
|
|
1
|
+
import { buildEffectiveTranslationKeys, featureHasI18nSurface, findTranslationLocaleGaps, requiredKeysFromFeature, requiredKeysFromNav, requiredKeysFromScreen, requiredKeysFromWorkspace, screenTitleKey, } from "../../i18n/required-surface-keys.js";
|
|
2
2
|
import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "../build-config-feature-schema.js";
|
|
3
3
|
import { EXTENSION_SELECTOR_HINT_KEY } from "../extension-selector-plugins.js";
|
|
4
4
|
import { createRegistry } from "../registry.js";
|
|
@@ -86,3 +86,10 @@ export function validateI18nSurfaceKeys(features) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
export function validateTranslationLocaleConsistency(features) {
|
|
90
|
+
const gaps = findTranslationLocaleGaps(features);
|
|
91
|
+
if (gaps.length > 0) {
|
|
92
|
+
const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing locale(s) ${gap.missingLocales.join(", ")}.`);
|
|
93
|
+
throw new Error(`${lines.join("\n")}\nEvery locale a feature uses must be maintained for every one of its translation keys; otherwise move that locale into a locale package.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -15,7 +15,7 @@ import { validateEntityListScreens } from "./entity-list-screens.js";
|
|
|
15
15
|
import { validateGdprStoragePersistence } from "./gdpr-storage.js";
|
|
16
16
|
import { validateGlobalTenancyEntities } from "./global-tenancy.js";
|
|
17
17
|
import { validateHandlerFieldReferences } from "./handler-field-references.js";
|
|
18
|
-
import { validateI18nSurfaceKeys } from "./i18n-keys.js";
|
|
18
|
+
import { validateI18nSurfaceKeys, validateTranslationLocaleConsistency } from "./i18n-keys.js";
|
|
19
19
|
import { collectKnownRoles, collectNavQns, collectWriteHandlerQns, validateNavCycles, validateNavs, warnOnNavAccessInversion, warnOnUnreachableNavScreens, } from "./nav.js";
|
|
20
20
|
import { collectClaimKeys, validateOwnershipRules } from "./ownership.js";
|
|
21
21
|
import { validateParentRefs } from "./parent-ref.js";
|
|
@@ -218,6 +218,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
218
218
|
}
|
|
219
219
|
validateDefaultWorkspaceUniqueness(allWorkspaceQns);
|
|
220
220
|
validateI18nSurfaceKeys(features);
|
|
221
|
+
validateTranslationLocaleConsistency(features);
|
|
221
222
|
validateEntityListScreens(features);
|
|
222
223
|
validateSubmitPrefilledHiddenFields(features);
|
|
223
224
|
validateCardMetaOverflow(features);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
1
|
import { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./entity-handler.js";
|
|
3
|
-
const log = createFallbackLogger("kumiko:boot");
|
|
4
2
|
// Framework-managed Timestamp-Spalten — dürfen als retention.reference
|
|
5
3
|
// genutzt werden auch wenn nicht in entity.fields deklariert.
|
|
6
4
|
const FRAMEWORK_TIMESTAMP_FIELDS = new Set([
|
|
@@ -146,6 +144,12 @@ export function validatePiiAndRetention(feature) {
|
|
|
146
144
|
if (annotCount > 1) {
|
|
147
145
|
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has multiple subject-key annotations (personal: "self" / "tenant" / { of: "<field>" }). Pick one — each field belongs to exactly one subject.`);
|
|
148
146
|
}
|
|
147
|
+
if ((field.type === "text" || field.type === "longText") &&
|
|
148
|
+
annotCount === 0 &&
|
|
149
|
+
!annot.allowPlaintext &&
|
|
150
|
+
!annot.subjectRef) {
|
|
151
|
+
throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" (type "${field.type}") declares no personal stance. Build it with createTextField / createLongTextField and { personal: "self" | "tenant" | "ref" | { of: "<ownerField>" } | false }; false additionally needs { reason: "..." } stating why the value is not personal data.`);
|
|
152
|
+
}
|
|
149
153
|
// lookupable (#818): nur text + nur MIT Subject-Annotation. Ohne
|
|
150
154
|
// Subject bleibt der Wert eh Klartext — der Blind-Index wäre eine
|
|
151
155
|
// zweite (deterministische!) Kopie ohne Nutzen.
|
|
@@ -247,11 +251,6 @@ export function validatePiiAndRetention(feature) {
|
|
|
247
251
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
248
252
|
console.warn(`[kumiko:boot] [Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has a user-reference-typical name but no personal annotation — a foreign key into \`user\` carries Art.17 obligations even with no annotated content on the entity. Mark it { personal: "ref" } AND register r.useExtension(EXT_USER_DATA, "${entityName}", …) for Art.17 coverage — this warning is the only boot-time check for that, registering the hook is not enforced. Or { personal: { of: "${fieldName}" } } on the field it owns. If business data, set { personal: false, reason: "..." } to silence.`);
|
|
249
253
|
}
|
|
250
|
-
else if (!annot.subjectRef && (field.type === "text" || field.type === "longText")) {
|
|
251
|
-
// The factories refuse a missing stance, but a raw literal such as
|
|
252
|
-
// { type: "text" } still compiles and reaches the entity unchecked.
|
|
253
|
-
log.warn(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" (type "${field.type}") declares no personal stance. Declare { personal: "self" | "tenant" | "ref" | { of: "<ownerField>" } | false } via createTextField / createLongTextField; "false" additionally needs { reason: "..." } stating why the value is not personal data.`);
|
|
254
|
-
}
|
|
255
254
|
}
|
|
256
255
|
}
|
|
257
256
|
// --- Entity-level retention ---
|
|
@@ -456,7 +456,7 @@ function assertValidScreenMultilineRows(featureName, shortId, screen) {
|
|
|
456
456
|
// `derived`, `totals`, `totalsMatch`) — a top-level `Array.isArray`/plain-
|
|
457
457
|
// object check alone would let a function survive one level deep.
|
|
458
458
|
//
|
|
459
|
-
// Cast at the exit `as
|
|
459
|
+
// Cast at the exit `as EntityFieldDefinition`: type-system-wise our out-map only
|
|
460
460
|
// satisfies the discriminated union with an unnarrowed `type` string — the
|
|
461
461
|
// cast bridges the variant inference TS can't recompute from a generic
|
|
462
462
|
// Record.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { EntityTenancy } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
2
|
-
import type { BigIntFieldDef, BooleanFieldDef, DateFieldDef, DecimalFieldDef, DerivedFieldDef, EmbeddedFieldDef, EntityDefinition, EntityIndexDef, FieldsMap, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, ResolvedPiiFlags, RetentionDef, SelectFieldDef, TextFieldDef, TimestampFieldDef, TzFieldDef } from "./types/index.ts";
|
|
2
|
+
import type { BigIntFieldDef, BooleanFieldDef, DateFieldDef, DecimalFieldDef, DerivedFieldDef, EmbeddedFieldDef, EntityDefinition, EntityIndexDef, EntityLongTextFieldDef, EntityTextFieldDef, FieldsMap, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, ResolvedPiiFlags, RetentionDef, SelectFieldDef, TextFieldDef, TimestampFieldDef, TzFieldDef } from "./types/index.ts";
|
|
3
3
|
export declare function createTextField<R extends true | false = false>(overrides: Partial<Omit<TextFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotations & {
|
|
4
4
|
required?: R;
|
|
5
|
-
}):
|
|
5
|
+
}): EntityTextFieldDef & {
|
|
6
6
|
required: R;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
@@ -25,7 +25,7 @@ export declare function createTextField<R extends true | false = false>(override
|
|
|
25
25
|
export declare function createDerivedField(spec: DerivedFieldDef): DerivedFieldDef;
|
|
26
26
|
export declare function createLongTextField<R extends true | false = false>(overrides: Partial<Omit<LongTextFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotationsLongText & {
|
|
27
27
|
required?: R;
|
|
28
|
-
}):
|
|
28
|
+
}): EntityLongTextFieldDef & {
|
|
29
29
|
required: R;
|
|
30
30
|
};
|
|
31
31
|
export declare function createBooleanField<R extends true | false = false>(overrides?: Partial<Omit<BooleanFieldDef, "type" | "required">> & {
|
|
@@ -6,7 +6,7 @@ export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap,
|
|
|
6
6
|
export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsMap, } from "@cosmicdrift/kumiko-types/extension-options-map";
|
|
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
|
-
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";
|
|
9
|
+
export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityFieldDefinition, EntityIndexDef, EntityLongTextFieldDef, EntityTextFieldDef, 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, ResolvedPiiStance, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
|
|
10
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, JobDeferral, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, ProviderPluginContext, 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
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";
|
|
@@ -1,72 +1,12 @@
|
|
|
1
1
|
export { FILE_REF_ENTITY_NAME } from "./types.ts";
|
|
2
2
|
export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
|
|
3
|
-
readonly storageKey: {
|
|
4
|
-
readonly type: "text";
|
|
5
|
-
readonly description?: string;
|
|
6
|
-
readonly maxLength?: number;
|
|
7
|
-
readonly minLength?: number;
|
|
8
|
-
readonly autoComplete?: string;
|
|
9
|
-
readonly required?: boolean;
|
|
10
|
-
readonly searchable?: boolean;
|
|
11
|
-
readonly sortable?: boolean;
|
|
12
|
-
readonly filterable?: boolean;
|
|
13
|
-
readonly encrypted?: boolean;
|
|
14
|
-
readonly sensitive?: boolean;
|
|
15
|
-
readonly writeOnly?: boolean;
|
|
16
|
-
readonly readAsInstruction?: boolean;
|
|
17
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
18
|
-
readonly default?: string;
|
|
19
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
20
|
-
readonly multiline?: boolean | {
|
|
21
|
-
readonly rows?: number;
|
|
22
|
-
};
|
|
23
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
3
|
+
readonly storageKey: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
24
4
|
required: true;
|
|
25
5
|
};
|
|
26
|
-
readonly fileName: {
|
|
27
|
-
readonly type: "text";
|
|
28
|
-
readonly description?: string;
|
|
29
|
-
readonly maxLength?: number;
|
|
30
|
-
readonly minLength?: number;
|
|
31
|
-
readonly autoComplete?: string;
|
|
32
|
-
readonly required?: boolean;
|
|
33
|
-
readonly searchable?: boolean;
|
|
34
|
-
readonly sortable?: boolean;
|
|
35
|
-
readonly filterable?: boolean;
|
|
36
|
-
readonly encrypted?: boolean;
|
|
37
|
-
readonly sensitive?: boolean;
|
|
38
|
-
readonly writeOnly?: boolean;
|
|
39
|
-
readonly readAsInstruction?: boolean;
|
|
40
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
41
|
-
readonly default?: string;
|
|
42
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
43
|
-
readonly multiline?: boolean | {
|
|
44
|
-
readonly rows?: number;
|
|
45
|
-
};
|
|
46
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
6
|
+
readonly fileName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
47
7
|
required: true;
|
|
48
8
|
};
|
|
49
|
-
readonly mimeType: {
|
|
50
|
-
readonly type: "text";
|
|
51
|
-
readonly description?: string;
|
|
52
|
-
readonly maxLength?: number;
|
|
53
|
-
readonly minLength?: number;
|
|
54
|
-
readonly autoComplete?: string;
|
|
55
|
-
readonly required?: boolean;
|
|
56
|
-
readonly searchable?: boolean;
|
|
57
|
-
readonly sortable?: boolean;
|
|
58
|
-
readonly filterable?: boolean;
|
|
59
|
-
readonly encrypted?: boolean;
|
|
60
|
-
readonly sensitive?: boolean;
|
|
61
|
-
readonly writeOnly?: boolean;
|
|
62
|
-
readonly readAsInstruction?: boolean;
|
|
63
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
64
|
-
readonly default?: string;
|
|
65
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
66
|
-
readonly multiline?: boolean | {
|
|
67
|
-
readonly rows?: number;
|
|
68
|
-
};
|
|
69
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
9
|
+
readonly mimeType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
70
10
|
required: true;
|
|
71
11
|
};
|
|
72
12
|
readonly size: {
|
|
@@ -81,73 +21,13 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
81
21
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
82
22
|
required: true;
|
|
83
23
|
};
|
|
84
|
-
readonly entityType: {
|
|
85
|
-
readonly type: "text";
|
|
86
|
-
readonly description?: string;
|
|
87
|
-
readonly maxLength?: number;
|
|
88
|
-
readonly minLength?: number;
|
|
89
|
-
readonly autoComplete?: string;
|
|
90
|
-
readonly required?: boolean;
|
|
91
|
-
readonly searchable?: boolean;
|
|
92
|
-
readonly sortable?: boolean;
|
|
93
|
-
readonly filterable?: boolean;
|
|
94
|
-
readonly encrypted?: boolean;
|
|
95
|
-
readonly sensitive?: boolean;
|
|
96
|
-
readonly writeOnly?: boolean;
|
|
97
|
-
readonly readAsInstruction?: boolean;
|
|
98
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
99
|
-
readonly default?: string;
|
|
100
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
101
|
-
readonly multiline?: boolean | {
|
|
102
|
-
readonly rows?: number;
|
|
103
|
-
};
|
|
104
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
24
|
+
readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
105
25
|
required: false;
|
|
106
26
|
};
|
|
107
|
-
readonly entityId: {
|
|
108
|
-
readonly type: "text";
|
|
109
|
-
readonly description?: string;
|
|
110
|
-
readonly maxLength?: number;
|
|
111
|
-
readonly minLength?: number;
|
|
112
|
-
readonly autoComplete?: string;
|
|
113
|
-
readonly required?: boolean;
|
|
114
|
-
readonly searchable?: boolean;
|
|
115
|
-
readonly sortable?: boolean;
|
|
116
|
-
readonly filterable?: boolean;
|
|
117
|
-
readonly encrypted?: boolean;
|
|
118
|
-
readonly sensitive?: boolean;
|
|
119
|
-
readonly writeOnly?: boolean;
|
|
120
|
-
readonly readAsInstruction?: boolean;
|
|
121
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
122
|
-
readonly default?: string;
|
|
123
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
124
|
-
readonly multiline?: boolean | {
|
|
125
|
-
readonly rows?: number;
|
|
126
|
-
};
|
|
127
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
27
|
+
readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
128
28
|
required: false;
|
|
129
29
|
};
|
|
130
|
-
readonly fieldName: {
|
|
131
|
-
readonly type: "text";
|
|
132
|
-
readonly description?: string;
|
|
133
|
-
readonly maxLength?: number;
|
|
134
|
-
readonly minLength?: number;
|
|
135
|
-
readonly autoComplete?: string;
|
|
136
|
-
readonly required?: boolean;
|
|
137
|
-
readonly searchable?: boolean;
|
|
138
|
-
readonly sortable?: boolean;
|
|
139
|
-
readonly filterable?: boolean;
|
|
140
|
-
readonly encrypted?: boolean;
|
|
141
|
-
readonly sensitive?: boolean;
|
|
142
|
-
readonly writeOnly?: boolean;
|
|
143
|
-
readonly readAsInstruction?: boolean;
|
|
144
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
145
|
-
readonly default?: string;
|
|
146
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
147
|
-
readonly multiline?: boolean | {
|
|
148
|
-
readonly rows?: number;
|
|
149
|
-
};
|
|
150
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
30
|
+
readonly fieldName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
151
31
|
required: false;
|
|
152
32
|
};
|
|
153
33
|
}, "tenant">;
|
|
@@ -1,71 +1,11 @@
|
|
|
1
1
|
export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable<import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
|
|
2
|
-
readonly storageKey: {
|
|
3
|
-
readonly type: "text";
|
|
4
|
-
readonly description?: string;
|
|
5
|
-
readonly maxLength?: number;
|
|
6
|
-
readonly minLength?: number;
|
|
7
|
-
readonly autoComplete?: string;
|
|
8
|
-
readonly required?: boolean;
|
|
9
|
-
readonly searchable?: boolean;
|
|
10
|
-
readonly sortable?: boolean;
|
|
11
|
-
readonly filterable?: boolean;
|
|
12
|
-
readonly encrypted?: boolean;
|
|
13
|
-
readonly sensitive?: boolean;
|
|
14
|
-
readonly writeOnly?: boolean;
|
|
15
|
-
readonly readAsInstruction?: boolean;
|
|
16
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
17
|
-
readonly default?: string;
|
|
18
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
19
|
-
readonly multiline?: boolean | {
|
|
20
|
-
readonly rows?: number;
|
|
21
|
-
};
|
|
22
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
2
|
+
readonly storageKey: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
23
3
|
required: true;
|
|
24
4
|
};
|
|
25
|
-
readonly fileName: {
|
|
26
|
-
readonly type: "text";
|
|
27
|
-
readonly description?: string;
|
|
28
|
-
readonly maxLength?: number;
|
|
29
|
-
readonly minLength?: number;
|
|
30
|
-
readonly autoComplete?: string;
|
|
31
|
-
readonly required?: boolean;
|
|
32
|
-
readonly searchable?: boolean;
|
|
33
|
-
readonly sortable?: boolean;
|
|
34
|
-
readonly filterable?: boolean;
|
|
35
|
-
readonly encrypted?: boolean;
|
|
36
|
-
readonly sensitive?: boolean;
|
|
37
|
-
readonly writeOnly?: boolean;
|
|
38
|
-
readonly readAsInstruction?: boolean;
|
|
39
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
40
|
-
readonly default?: string;
|
|
41
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
42
|
-
readonly multiline?: boolean | {
|
|
43
|
-
readonly rows?: number;
|
|
44
|
-
};
|
|
45
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
5
|
+
readonly fileName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
46
6
|
required: true;
|
|
47
7
|
};
|
|
48
|
-
readonly mimeType: {
|
|
49
|
-
readonly type: "text";
|
|
50
|
-
readonly description?: string;
|
|
51
|
-
readonly maxLength?: number;
|
|
52
|
-
readonly minLength?: number;
|
|
53
|
-
readonly autoComplete?: string;
|
|
54
|
-
readonly required?: boolean;
|
|
55
|
-
readonly searchable?: boolean;
|
|
56
|
-
readonly sortable?: boolean;
|
|
57
|
-
readonly filterable?: boolean;
|
|
58
|
-
readonly encrypted?: boolean;
|
|
59
|
-
readonly sensitive?: boolean;
|
|
60
|
-
readonly writeOnly?: boolean;
|
|
61
|
-
readonly readAsInstruction?: boolean;
|
|
62
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
63
|
-
readonly default?: string;
|
|
64
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
65
|
-
readonly multiline?: boolean | {
|
|
66
|
-
readonly rows?: number;
|
|
67
|
-
};
|
|
68
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
8
|
+
readonly mimeType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
69
9
|
required: true;
|
|
70
10
|
};
|
|
71
11
|
readonly size: {
|
|
@@ -80,73 +20,13 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
|
|
|
80
20
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
81
21
|
required: true;
|
|
82
22
|
};
|
|
83
|
-
readonly entityType: {
|
|
84
|
-
readonly type: "text";
|
|
85
|
-
readonly description?: string;
|
|
86
|
-
readonly maxLength?: number;
|
|
87
|
-
readonly minLength?: number;
|
|
88
|
-
readonly autoComplete?: string;
|
|
89
|
-
readonly required?: boolean;
|
|
90
|
-
readonly searchable?: boolean;
|
|
91
|
-
readonly sortable?: boolean;
|
|
92
|
-
readonly filterable?: boolean;
|
|
93
|
-
readonly encrypted?: boolean;
|
|
94
|
-
readonly sensitive?: boolean;
|
|
95
|
-
readonly writeOnly?: boolean;
|
|
96
|
-
readonly readAsInstruction?: boolean;
|
|
97
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
98
|
-
readonly default?: string;
|
|
99
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
100
|
-
readonly multiline?: boolean | {
|
|
101
|
-
readonly rows?: number;
|
|
102
|
-
};
|
|
103
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
23
|
+
readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
104
24
|
required: false;
|
|
105
25
|
};
|
|
106
|
-
readonly entityId: {
|
|
107
|
-
readonly type: "text";
|
|
108
|
-
readonly description?: string;
|
|
109
|
-
readonly maxLength?: number;
|
|
110
|
-
readonly minLength?: number;
|
|
111
|
-
readonly autoComplete?: string;
|
|
112
|
-
readonly required?: boolean;
|
|
113
|
-
readonly searchable?: boolean;
|
|
114
|
-
readonly sortable?: boolean;
|
|
115
|
-
readonly filterable?: boolean;
|
|
116
|
-
readonly encrypted?: boolean;
|
|
117
|
-
readonly sensitive?: boolean;
|
|
118
|
-
readonly writeOnly?: boolean;
|
|
119
|
-
readonly readAsInstruction?: boolean;
|
|
120
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
121
|
-
readonly default?: string;
|
|
122
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
123
|
-
readonly multiline?: boolean | {
|
|
124
|
-
readonly rows?: number;
|
|
125
|
-
};
|
|
126
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
26
|
+
readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
127
27
|
required: false;
|
|
128
28
|
};
|
|
129
|
-
readonly fieldName: {
|
|
130
|
-
readonly type: "text";
|
|
131
|
-
readonly description?: string;
|
|
132
|
-
readonly maxLength?: number;
|
|
133
|
-
readonly minLength?: number;
|
|
134
|
-
readonly autoComplete?: string;
|
|
135
|
-
readonly required?: boolean;
|
|
136
|
-
readonly searchable?: boolean;
|
|
137
|
-
readonly sortable?: boolean;
|
|
138
|
-
readonly filterable?: boolean;
|
|
139
|
-
readonly encrypted?: boolean;
|
|
140
|
-
readonly sensitive?: boolean;
|
|
141
|
-
readonly writeOnly?: boolean;
|
|
142
|
-
readonly readAsInstruction?: boolean;
|
|
143
|
-
readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
|
|
144
|
-
readonly default?: string;
|
|
145
|
-
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
146
|
-
readonly multiline?: boolean | {
|
|
147
|
-
readonly rows?: number;
|
|
148
|
-
};
|
|
149
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
29
|
+
readonly fieldName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
150
30
|
required: false;
|
|
151
31
|
};
|
|
152
32
|
}, "tenant">>;
|
|
@@ -23,6 +23,9 @@ const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
|
|
|
23
23
|
// leaked URL (e.g. from a browser history screenshot) isn't a long-lived
|
|
24
24
|
// credential (see "Signed-URL default expiry" in core-files.md).
|
|
25
25
|
const SIGNED_URL_DEFAULT_EXPIRY_SECONDS = 15 * 60;
|
|
26
|
+
const MAX_BATCH_META_IDS = 100;
|
|
27
|
+
// Each detail() read decrypts through the KMS pool; a bounded chunk keeps one batch from hogging it.
|
|
28
|
+
const BATCH_META_CONCURRENCY = 10;
|
|
26
29
|
// Default guard: on attached files, allow the uploader or a privileged role,
|
|
27
30
|
// and for reads also everyone who may read the record. Delete stays with the
|
|
28
31
|
// uploader and privileged roles: reading a record is no right to remove its
|
|
@@ -218,6 +221,25 @@ export function createFileRoutes(options) {
|
|
|
218
221
|
return c.json({ error: "upload_failed" }, 500);
|
|
219
222
|
return c.json(outcome.fileRef, 201);
|
|
220
223
|
});
|
|
224
|
+
// GET /files/meta?ids=a,b,c — meta of many files in one request (list/card
|
|
225
|
+
// cells otherwise fire one request per row). Unreadable and unknown ids are
|
|
226
|
+
// left out of the answer, same as /files/:id/meta answering 404 for them.
|
|
227
|
+
// Registered before /files/:id so "meta" is never taken for an id.
|
|
228
|
+
api.get("/files/meta", async (c) => {
|
|
229
|
+
const user = getUser(c);
|
|
230
|
+
const ids = [...new Set((c.req.query("ids") ?? "").split(",").filter((id) => isUuid(id)))];
|
|
231
|
+
if (ids.length > MAX_BATCH_META_IDS)
|
|
232
|
+
return c.json({ error: "too_many_ids" }, 400);
|
|
233
|
+
const files = [];
|
|
234
|
+
for (let start = 0; start < ids.length; start += BATCH_META_CONCURRENCY) {
|
|
235
|
+
const chunk = ids.slice(start, start + BATCH_META_CONCURRENCY);
|
|
236
|
+
const metas = await Promise.all(chunk.map((id) => loadReadableFileMeta(id, user)));
|
|
237
|
+
for (const meta of metas)
|
|
238
|
+
if (meta)
|
|
239
|
+
files.push(meta);
|
|
240
|
+
}
|
|
241
|
+
return c.json({ files });
|
|
242
|
+
});
|
|
221
243
|
// GET /files/:id — download.
|
|
222
244
|
//
|
|
223
245
|
// Authorization stack:
|
|
@@ -388,31 +410,28 @@ export function createFileRoutes(options) {
|
|
|
388
410
|
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000).toISOString();
|
|
389
411
|
return c.json({ url, expiresAt });
|
|
390
412
|
});
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
const user = getUser(c);
|
|
397
|
-
const id = c.req.param("id");
|
|
413
|
+
// Metadata without the bytes. Unlike the byte-serving routes, this returns
|
|
414
|
+
// fileName as JSON — and fileName is `personal: "self"` (fw#2423), so it must
|
|
415
|
+
// go through the entity executor's detail() to decrypt instead of
|
|
416
|
+
// loadFileForTenant's raw, still-encrypted read.
|
|
417
|
+
async function loadReadableFileMeta(id, user) {
|
|
398
418
|
// Same 22P02-avoidance as loadFileForTenant — detail() doesn't validate
|
|
399
419
|
// id shape before it hits the UUID column.
|
|
400
420
|
if (!isUuid(id))
|
|
401
|
-
return
|
|
421
|
+
return null;
|
|
402
422
|
const row = await executor.detail({ id }, user, tenantDbFor(user.tenantId));
|
|
403
423
|
if (!row)
|
|
404
|
-
return
|
|
424
|
+
return null;
|
|
405
425
|
const fileRef = row; // @cast-boundary db-row (decrypted via executor.detail)
|
|
406
426
|
// detail()'s "pass"-ownership read widens to tenantId IN (self, SYSTEM),
|
|
407
427
|
// narrower in loadFileForTenant's selectMany. Restore that here so meta
|
|
408
428
|
// doesn't leak reference-tenant rows.
|
|
409
|
-
if (fileRef.tenantId !== user.tenantId)
|
|
410
|
-
return
|
|
411
|
-
}
|
|
429
|
+
if (fileRef.tenantId !== user.tenantId)
|
|
430
|
+
return null;
|
|
412
431
|
const decision = await guard({ fileRef, user, operation: "read" });
|
|
413
432
|
if (decision === "deny")
|
|
414
|
-
return
|
|
415
|
-
return
|
|
433
|
+
return null;
|
|
434
|
+
return {
|
|
416
435
|
id: fileRef.id,
|
|
417
436
|
fileName: fileRef.fileName,
|
|
418
437
|
mimeType: fileRef.mimeType,
|
|
@@ -420,7 +439,11 @@ export function createFileRoutes(options) {
|
|
|
420
439
|
entityType: fileRef.entityType,
|
|
421
440
|
entityId: fileRef.entityId,
|
|
422
441
|
fieldName: fileRef.fieldName,
|
|
423
|
-
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
api.get("/files/:id/meta", async (c) => {
|
|
445
|
+
const meta = await loadReadableFileMeta(c.req.param("id"), getUser(c));
|
|
446
|
+
return meta ? c.json(meta) : c.json({ error: "not_found" }, 404);
|
|
424
447
|
});
|
|
425
448
|
async function loadFileForTenant(id, tenantId) {
|
|
426
449
|
// fileRefsTable.id is a UUID column — a malformed id must fail here, not
|
|
@@ -31,5 +31,9 @@ export type TranslationLocaleGap = {
|
|
|
31
31
|
readonly key: string;
|
|
32
32
|
readonly missingLocales: readonly string[];
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Per feature, every translation key must carry every locale that any key of the
|
|
36
|
+
* same feature carries. A feature with a single locale across all keys is
|
|
37
|
+
* consistent by definition (locale packages, en-only framework features).
|
|
38
|
+
*/
|
|
39
|
+
export declare function findTranslationLocaleGaps(features: readonly FeatureDefinition[]): readonly TranslationLocaleGap[];
|
|
@@ -519,7 +519,33 @@ export function buildEffectiveTranslationKeys(features) {
|
|
|
519
519
|
}
|
|
520
520
|
return out;
|
|
521
521
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
522
|
+
function hasLocaleValue(value) {
|
|
523
|
+
if (value === undefined)
|
|
524
|
+
return false;
|
|
525
|
+
return typeof value === "string" ? value !== "" : true;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Per feature, every translation key must carry every locale that any key of the
|
|
529
|
+
* same feature carries. A feature with a single locale across all keys is
|
|
530
|
+
* consistent by definition (locale packages, en-only framework features).
|
|
531
|
+
*/
|
|
532
|
+
export function findTranslationLocaleGaps(features) {
|
|
533
|
+
const gaps = [];
|
|
534
|
+
for (const feature of features) {
|
|
535
|
+
const entries = Object.entries(feature.translations ?? {});
|
|
536
|
+
const featureLocales = new Set();
|
|
537
|
+
for (const [, byLocale] of entries) {
|
|
538
|
+
for (const locale of Object.keys(byLocale))
|
|
539
|
+
featureLocales.add(locale);
|
|
540
|
+
}
|
|
541
|
+
if (featureLocales.size < 2)
|
|
542
|
+
continue;
|
|
543
|
+
const sortedLocales = [...featureLocales].sort();
|
|
544
|
+
for (const [key, byLocale] of entries) {
|
|
545
|
+
const missingLocales = sortedLocales.filter((locale) => !hasLocaleValue(byLocale[locale]));
|
|
546
|
+
if (missingLocales.length > 0)
|
|
547
|
+
gaps.push({ featureName: feature.name, key, missingLocales });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return gaps;
|
|
525
551
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
|
-
import type {
|
|
2
|
+
import type { DbRunner } from "../db/connection.ts";
|
|
3
|
+
export type ConsumerTransitionRejection = "unknown-consumer" | "wrong-status";
|
|
4
|
+
export declare class ConsumerTransitionError extends Error {
|
|
5
|
+
readonly rejection: ConsumerTransitionRejection;
|
|
6
|
+
constructor(rejection: ConsumerTransitionRejection, message: string);
|
|
7
|
+
}
|
|
3
8
|
export type ConsumerRecoveryState = {
|
|
4
9
|
readonly name: string;
|
|
5
10
|
readonly instanceId: string;
|
|
@@ -10,13 +15,15 @@ export type ConsumerRecoveryState = {
|
|
|
10
15
|
readonly lastError: string | null;
|
|
11
16
|
readonly updatedAt: Temporal.Instant;
|
|
12
17
|
};
|
|
13
|
-
export declare function restartConsumer(db:
|
|
14
|
-
export declare function disableConsumer(db:
|
|
15
|
-
export declare function enableConsumer(db:
|
|
16
|
-
export declare function skipPoisonEvent(db:
|
|
18
|
+
export declare function restartConsumer(db: DbRunner, name: string, instanceId?: string): Promise<ConsumerRecoveryState>;
|
|
19
|
+
export declare function disableConsumer(db: DbRunner, name: string, instanceId?: string): Promise<ConsumerRecoveryState>;
|
|
20
|
+
export declare function enableConsumer(db: DbRunner, name: string, instanceId?: string): Promise<ConsumerRecoveryState>;
|
|
21
|
+
export declare function skipPoisonEvent(db: DbRunner, name: string, instanceId?: string, options?: {
|
|
22
|
+
readonly requireStatus?: string;
|
|
23
|
+
}): Promise<ConsumerRecoveryState & {
|
|
17
24
|
readonly skippedEventId: bigint | null;
|
|
18
25
|
}>;
|
|
19
|
-
export declare function getConsumerState(db:
|
|
26
|
+
export declare function getConsumerState(db: DbRunner, name: string, instanceId?: string): Promise<{
|
|
20
27
|
readonly name: string;
|
|
21
28
|
readonly instanceId: string;
|
|
22
29
|
readonly status: string;
|
|
@@ -26,7 +33,7 @@ export declare function getConsumerState(db: DbConnection, name: string, instanc
|
|
|
26
33
|
readonly lastError: string | null;
|
|
27
34
|
readonly updatedAt: Temporal.Instant;
|
|
28
35
|
} | null>;
|
|
29
|
-
export declare function listConsumersWithState(db:
|
|
36
|
+
export declare function listConsumersWithState(db: DbRunner, registeredNames: readonly string[]): Promise<ReadonlyArray<{
|
|
30
37
|
readonly name: string;
|
|
31
38
|
readonly instanceId: string;
|
|
32
39
|
readonly status: string;
|
|
@@ -44,4 +51,4 @@ export type ConsumerProgress = {
|
|
|
44
51
|
readonly highWaterMark: bigint;
|
|
45
52
|
readonly lag: bigint;
|
|
46
53
|
};
|
|
47
|
-
export declare function getAllConsumerProgress(db:
|
|
54
|
+
export declare function getAllConsumerProgress(db: DbRunner, registeredNames: readonly string[]): Promise<readonly ConsumerProgress[]>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectConsumerForUpdate, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
|
|
3
|
-
import { coerceRow, extractTableInfo, selectMany } from "../db/query.js";
|
|
3
|
+
import { coerceRow, extractTableInfo, runInNewTransaction, runInSavepointIfSupported, runnerCanBeginTransaction, selectMany, } from "../db/query.js";
|
|
4
4
|
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
5
5
|
import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
6
6
|
import { mergeContiguousGapRanges, rangeContainsId, splitRangeExcludingIds, toIdRanges, } from "./pending-gap-ranges.js";
|
|
@@ -33,6 +33,14 @@ function normalizeConsumerState(row) {
|
|
|
33
33
|
updatedAt: row.updatedAt,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
export class ConsumerTransitionError extends Error {
|
|
37
|
+
rejection;
|
|
38
|
+
constructor(rejection, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.rejection = rejection;
|
|
41
|
+
this.name = "ConsumerTransitionError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
36
44
|
// Ops calls default to the SHARED_INSTANCE_SENTINEL row — that's the only
|
|
37
45
|
// row shared-delivery consumers have, so legacy CLI invocations without
|
|
38
46
|
// --instance-id keep working. Per-instance consumers require an explicit
|
|
@@ -45,7 +53,7 @@ async function requireConsumerRow(db, name, instanceId) {
|
|
|
45
53
|
instanceId,
|
|
46
54
|
});
|
|
47
55
|
if (!row) {
|
|
48
|
-
throw new
|
|
56
|
+
throw new ConsumerTransitionError("unknown-consumer", `Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
|
|
49
57
|
`For per-instance consumers pass the instance_id explicitly; shared consumers use the default.`);
|
|
50
58
|
}
|
|
51
59
|
return row;
|
|
@@ -53,7 +61,7 @@ async function requireConsumerRow(db, name, instanceId) {
|
|
|
53
61
|
async function requireConsumerRowForUpdate(tx, name, instanceId) {
|
|
54
62
|
const raw = await selectConsumerForUpdate(tx, name, instanceId);
|
|
55
63
|
if (!raw) {
|
|
56
|
-
throw new
|
|
64
|
+
throw new ConsumerTransitionError("unknown-consumer", `Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
|
|
57
65
|
`For per-instance consumers pass the instance_id explicitly; shared consumers use the default.`);
|
|
58
66
|
}
|
|
59
67
|
return coerceRow(raw, extractTableInfo(eventConsumerStateTable));
|
|
@@ -69,7 +77,7 @@ async function applyConsumerStatusTransition(db, name, instanceId, targetStatus)
|
|
|
69
77
|
export async function restartConsumer(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
|
|
70
78
|
const before = await requireConsumerRow(db, name, instanceId);
|
|
71
79
|
if (before.status !== "dead") {
|
|
72
|
-
throw new
|
|
80
|
+
throw new ConsumerTransitionError("wrong-status", `Consumer "${name}" (instance_id="${instanceId}") is not dead (status="${before.status}"). Restart only applies to dead consumers; use "enable" for a disabled one.`);
|
|
73
81
|
}
|
|
74
82
|
return applyConsumerStatusTransition(db, name, instanceId, "idle");
|
|
75
83
|
}
|
|
@@ -80,10 +88,17 @@ export async function disableConsumer(db, name, instanceId = SHARED_INSTANCE_SEN
|
|
|
80
88
|
export async function enableConsumer(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
|
|
81
89
|
const before = await requireConsumerRow(db, name, instanceId);
|
|
82
90
|
if (before.status !== "disabled") {
|
|
83
|
-
throw new
|
|
91
|
+
throw new ConsumerTransitionError("wrong-status", `Consumer "${name}" (instance_id="${instanceId}") is not disabled (status="${before.status}"). Enable only flips disabled → idle; use "restart" for a dead consumer.`);
|
|
84
92
|
}
|
|
85
93
|
return applyConsumerStatusTransition(db, name, instanceId, "idle");
|
|
86
94
|
}
|
|
95
|
+
// A pool connection opens its own transaction; inside a handler transaction the
|
|
96
|
+
// locked read and writes join it, confined to a savepoint.
|
|
97
|
+
function runInOwnTransactionOrSavepoint(db, fn) {
|
|
98
|
+
return runnerCanBeginTransaction(db)
|
|
99
|
+
? runInNewTransaction(db, fn)
|
|
100
|
+
: runInSavepointIfSupported(db, fn);
|
|
101
|
+
}
|
|
87
102
|
// Delivery stops at the poison event, so ids between the cursor and it were
|
|
88
103
|
// invisible during that pass (in-flight commits); keeping them as a pending gap
|
|
89
104
|
// avoids losing them when the cursor jumps over.
|
|
@@ -97,6 +112,11 @@ async function invisibleWindowGap(tx, cursor, poisonId) {
|
|
|
97
112
|
xmax: await selectSnapshotXmax(tx),
|
|
98
113
|
};
|
|
99
114
|
}
|
|
115
|
+
function assertRequiredStatus(row, name, instanceId, requiredStatus) {
|
|
116
|
+
if (requiredStatus !== undefined && row.status !== requiredStatus) {
|
|
117
|
+
throw new ConsumerTransitionError("wrong-status", `Consumer "${name}" (instance_id="${instanceId}") is "${row.status}", not "${requiredStatus}"; nothing skipped.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
100
120
|
// skipPoisonEvent skips the event whose handler failed last
|
|
101
121
|
// (last_failed_event_id, set by the delivery's halt-on-poison branch): inside
|
|
102
122
|
// pending_gaps it is split out of its range, otherwise the cursor moves onto
|
|
@@ -104,11 +124,12 @@ async function invisibleWindowGap(tx, cursor, poisonId) {
|
|
|
104
124
|
// falls back to a guess: the smallest visible pending-gap id, else the next
|
|
105
125
|
// event after the cursor. Single TX so concurrent dispatcher passes can't
|
|
106
126
|
// double-advance. Nothing to skip → idempotent no-op.
|
|
107
|
-
export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
|
|
108
|
-
return db
|
|
127
|
+
export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SENTINEL, options = {}) {
|
|
128
|
+
return runInOwnTransactionOrSavepoint(db, async (tx) => {
|
|
109
129
|
// Locked read: a dispatcher turn committing new gaps between an unlocked
|
|
110
130
|
// read and the pending_gaps overwrite below would have them dropped.
|
|
111
131
|
const before = await requireConsumerRowForUpdate(tx, name, instanceId);
|
|
132
|
+
assertRequiredStatus(before, name, instanceId, options.requireStatus);
|
|
112
133
|
const pendingGaps = before.pendingGaps;
|
|
113
134
|
const failedEventId = before.lastFailedEventId;
|
|
114
135
|
const failedIsPending = failedEventId !== null && pendingGaps.some((gap) => rangeContainsId(gap, failedEventId));
|
|
@@ -49,5 +49,5 @@ export type EventDispatcherOptions = {
|
|
|
49
49
|
readonly pgClient?: PgListenClient;
|
|
50
50
|
};
|
|
51
51
|
export declare function createEventDispatcher(options: EventDispatcherOptions): EventDispatcher;
|
|
52
|
-
export type { ConsumerProgress, ConsumerRecoveryState } from "./event-dispatcher-admin.ts";
|
|
53
|
-
export { disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher-admin.ts";
|
|
52
|
+
export type { ConsumerProgress, ConsumerRecoveryState, ConsumerTransitionRejection, } from "./event-dispatcher-admin.ts";
|
|
53
|
+
export { ConsumerTransitionError, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher-admin.ts";
|
|
@@ -573,4 +573,4 @@ export function createEventDispatcher(options) {
|
|
|
573
573
|
runOnce,
|
|
574
574
|
};
|
|
575
575
|
}
|
|
576
|
-
export { disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher-admin.js";
|
|
576
|
+
export { ConsumerTransitionError, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher-admin.js";
|
package/dist/pipeline/index.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export type { ConsumerStatus, PendingGapEntry } from "./event-consumer-state.ts"
|
|
|
19
19
|
export { ConsumerStatuses, createEventConsumerStateTable, eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.ts";
|
|
20
20
|
export type { EventDedup } from "./event-dedup.ts";
|
|
21
21
|
export { createEventDedup } from "./event-dedup.ts";
|
|
22
|
-
export type { ConsumerProgress, ConsumerRecoveryState, DispatcherPassResult, EventConsumer, EventConsumerBatchHandler, EventConsumerHandler, EventDispatcher, EventDispatcherOptions, } from "./event-dispatcher.ts";
|
|
23
|
-
export { createEventDispatcher, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher.ts";
|
|
22
|
+
export type { ConsumerProgress, ConsumerRecoveryState, ConsumerTransitionRejection, DispatcherPassResult, EventConsumer, EventConsumerBatchHandler, EventConsumerHandler, EventDispatcher, EventDispatcherOptions, } from "./event-dispatcher.ts";
|
|
23
|
+
export { ConsumerTransitionError, createEventDispatcher, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher.ts";
|
|
24
24
|
export type { ConsumerCursor } from "./event-dispatcher-delivery.ts";
|
|
25
25
|
export { selectConsumerCursorForUpdate } from "./event-dispatcher-delivery.ts";
|
|
26
26
|
export type { PruneEventsOptions, PruneEventsResult } from "./event-retention.ts";
|
package/dist/pipeline/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { createDistributedLock } from "./distributed-lock.js";
|
|
|
10
10
|
export { createEntityCache } from "./entity-cache.js";
|
|
11
11
|
export { ConsumerStatuses, createEventConsumerStateTable, eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
12
12
|
export { createEventDedup } from "./event-dedup.js";
|
|
13
|
-
export { createEventDispatcher, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher.js";
|
|
13
|
+
export { ConsumerTransitionError, createEventDispatcher, disableConsumer, enableConsumer, getAllConsumerProgress, getConsumerState, listConsumersWithState, restartConsumer, skipPoisonEvent, } from "./event-dispatcher.js";
|
|
14
14
|
export { selectConsumerCursorForUpdate } from "./event-dispatcher-delivery.js";
|
|
15
15
|
export { ConsumerLagError, pruneEvents } from "./event-retention.js";
|
|
16
16
|
export { createIdempotencyGuard } from "./idempotency.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.384.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"./package.json": "./package.json"
|
|
200
200
|
},
|
|
201
201
|
"dependencies": {
|
|
202
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
202
|
+
"@cosmicdrift/kumiko-types": "0.384.0",
|
|
203
203
|
"bullmq": "^5.76.7",
|
|
204
204
|
"bun-types": "^1.3.13",
|
|
205
205
|
"hono": "^4.13.1",
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"zod": "^4.4.3"
|
|
216
216
|
},
|
|
217
217
|
"devDependencies": {
|
|
218
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
218
|
+
"@cosmicdrift/kumiko-testing": "0.384.0",
|
|
219
219
|
"bun-types": "^1.3.13",
|
|
220
220
|
"pino-pretty": "^13.1.3"
|
|
221
221
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.384.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Boot fails when a feature leaves a locale out of some of its translation keys",
|
|
6
|
+
"detail": "Within one feature, every translation key must carry every locale that any key of the same feature carries. A missing locale or an empty string fails boot with one error listing all gaps. Features with a single locale across all keys are unaffected, as are locale packages. A plural object counts as present.",
|
|
7
|
+
"migration": "Add the missing locale to each listed key, or move that locale out of the feature into a locale package."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.384.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "Entity text fields must declare a personal stance",
|
|
13
|
+
"detail": "FieldsMap requires the personal stance of text and longText fields by type, and boot throws for such a field without one. The name heuristic for PII-typical names stays a warning and only applies to other field types.",
|
|
14
|
+
"migration": "Move raw `{ type: \"text\" }` and `{ type: \"longText\" }` entity fields to createTextField / createLongTextField with `personal` (\"self\", \"tenant\", \"ref\", `{ of }`, or false with a `reason`). tsc shows each place."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.384.0",
|
|
18
|
+
"type": "improvement",
|
|
19
|
+
"title": "Consumer admin functions accept a transaction, notIn where operator",
|
|
20
|
+
"detail": "restartConsumer, skipPoisonEvent, disableConsumer, enableConsumer and the progress readers accept a DbRunner (pool or open transaction); skipPoisonEvent uses its own transaction on a pool and a savepoint inside a handler transaction. skipPoisonEvent takes { requireStatus } and checks it under its row lock; the consumer-skip handler requires \"dead\". Rejected transitions throw ConsumerTransitionError with rejection \"unknown-consumer\" or \"wrong-status\". Where clauses support { notIn: [...] }. Existing callers passing a DbConnection compile unchanged."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"version": "0.384.0",
|
|
24
|
+
"type": "improvement",
|
|
25
|
+
"title": "GET /files/meta?ids= returns file metadata in one request",
|
|
26
|
+
"detail": "Up to 100 comma-separated file ids per call (more answers 400 too_many_ids). The same tenant and access checks as /files/:id/meta apply per id; unknown, foreign and invalid ids are left out of the answer. The renderer's file thumbnails and file names batch their lookups through it, so a list of N files no longer issues N meta requests."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"version": "0.383.1",
|
|
30
|
+
"type": "fix",
|
|
31
|
+
"title": "close() ends unbounded pools of abandoned long runs",
|
|
32
|
+
"detail": "withoutStatementTimeout (reindexEntity, PII backfill, projection and multi-stream rebuilds, event-store index builds) runs on its own pool without statement_timeout and closes it when the run settles. A run that never settles, such as a reindex a boot rebuild gave up on via its own timeout, kept that pool connected, so DROP DATABASE in test cleanup failed with \"is being accessed by other users\". The connection handle's close() now ends these pools too. A rebuild that is still running when close() is called is ended after the same close timeout instead of keeping shutdown waiting."
|
|
33
|
+
},
|
|
2
34
|
{
|
|
3
35
|
"version": "0.383.0",
|
|
4
36
|
"type": "breaking",
|