@cosmicdrift/kumiko-framework 0.359.0 → 0.361.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/dist/api/anonymous-cookie.js +9 -12
- package/dist/api/api-constants.d.ts +0 -1
- package/dist/api/api-constants.js +0 -1
- package/dist/api/auth-middleware.d.ts +0 -2
- package/dist/api/auth-middleware.js +16 -9
- package/dist/api/auth-routes.d.ts +2 -0
- package/dist/api/auth-routes.js +28 -40
- package/dist/api/csrf-middleware.js +3 -2
- package/dist/api/index.d.ts +4 -1
- package/dist/api/index.js +3 -1
- package/dist/api/origin-middleware.d.ts +11 -1
- package/dist/api/origin-middleware.js +40 -7
- package/dist/api/server.js +30 -4
- package/dist/api/session-cookies.d.ts +23 -0
- package/dist/api/session-cookies.js +85 -0
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +1 -1
- package/dist/crypto/owner-transition-rekey.d.ts +2 -0
- package/dist/crypto/owner-transition-rekey.js +30 -0
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +5 -0
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +14 -1
- package/dist/db/apply-entity-event.js +0 -5
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-field-encryption.js +3 -2
- package/dist/db/entity-table-meta.js +3 -3
- package/dist/db/event-store-executor-context.d.ts +1 -0
- package/dist/db/event-store-executor-context.js +9 -2
- package/dist/db/event-store-executor-write.js +31 -5
- package/dist/db/event-store-executor.d.ts +2 -1
- package/dist/db/event-store-executor.js +1 -0
- package/dist/db/index.d.ts +3 -3
- package/dist/db/index.js +2 -2
- package/dist/db/migrate-generator.d.ts +1 -0
- package/dist/db/migrate-generator.js +12 -0
- package/dist/db/queries/backfill-pii.d.ts +2 -0
- package/dist/db/queries/backfill-pii.js +120 -15
- package/dist/db/table-builder.d.ts +3 -1
- package/dist/db/table-builder.js +3 -6
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +38 -1
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/query-output-columns.js +3 -0
- package/dist/engine/boot-validator/screens.d.ts +1 -1
- package/dist/engine/boot-validator/screens.js +89 -14
- package/dist/engine/build-app-schema.js +1 -1
- package/dist/engine/define-handler.js +1 -3
- package/dist/engine/factories.d.ts +2 -0
- package/dist/engine/feature-config-events-jobs.js +8 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/schema-builder.js +2 -2
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +9 -0
- package/dist/engine/soft-delete-cleanup.js +12 -1
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/errors/classes.d.ts +7 -0
- package/dist/errors/classes.js +12 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +8 -0
- package/dist/files/assert-file-refs-belong-to-tenant.js +50 -0
- package/dist/files/bind-pending-file-refs.d.ts +9 -0
- package/dist/files/bind-pending-file-refs.js +54 -0
- package/dist/files/create-file-ref-from-bytes.d.ts +22 -0
- package/dist/files/create-file-ref-from-bytes.js +39 -0
- package/dist/files/create-file-ref.d.ts +50 -0
- package/dist/files/create-file-ref.js +135 -0
- package/dist/files/dispose-removed-file-refs.d.ts +8 -0
- package/dist/files/dispose-removed-file-refs.js +51 -0
- package/dist/files/feature.js +2 -2
- package/dist/files/file-ref-entity.d.ts +1 -0
- package/dist/files/file-ref-entity.js +1 -0
- package/dist/files/file-ref-table.js +2 -2
- package/dist/files/file-routes.d.ts +4 -0
- package/dist/files/file-routes.js +60 -126
- package/dist/files/index.d.ts +2 -0
- package/dist/files/index.js +1 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +14 -0
- package/dist/files/purge-expired-file-ref-bytes.js +34 -0
- package/dist/files/record-read-check.d.ts +9 -0
- package/dist/files/record-read-check.js +49 -0
- package/dist/files/record-write-check.d.ts +21 -0
- package/dist/files/record-write-check.js +64 -0
- package/dist/files/types.d.ts +1 -0
- package/dist/files/types.js +3 -0
- package/dist/i18n/required-surface-keys.js +10 -0
- package/dist/jobs/cron-repeat-options.d.ts +9 -0
- package/dist/jobs/cron-repeat-options.js +5 -0
- package/dist/jobs/job-runner.js +2 -1
- package/dist/migrations/pii-event-backfill.d.ts +1 -0
- package/dist/migrations/pii-event-backfill.js +16 -2
- package/dist/pipeline/cascade-handler.js +17 -2
- package/dist/pipeline/dispatch-write.js +7 -0
- package/dist/schema-cli.js +12 -6
- package/dist/search/reindex-entity.js +3 -4
- package/dist/stack/test-stack.d.ts +3 -1
- package/dist/stack/test-stack.js +1 -0
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +135 -0
package/dist/jobs/job-runner.js
CHANGED
|
@@ -17,6 +17,7 @@ import { RedisKeys } from "../pipeline/redis-keys.js";
|
|
|
17
17
|
import { createGatedIdentitySwitch } from "../pipeline/system-identity-switch.js";
|
|
18
18
|
import { buildPersonalDataGate, isPersonalDataGated, parseWriteOrigin, } from "../pipeline/write-origin.js";
|
|
19
19
|
import { bridgeStub } from "../testing/handler-context.js";
|
|
20
|
+
import { cronRepeatOptions } from "./cron-repeat-options.js";
|
|
20
21
|
// Shape-checked after the BullMQ/Redis JSON round-trip. Unlike `_writeOrigin`
|
|
21
22
|
// (a security-relevant escape hatch that must fail the run closed), a
|
|
22
23
|
// malformed `_triggerEvent` only costs an idempotency-key convenience — the
|
|
@@ -1154,7 +1155,7 @@ export function createJobRunner(options) {
|
|
|
1154
1155
|
if ("cron" in jobDef.trigger) {
|
|
1155
1156
|
const schedulerId = schedulerIdForJobName(name);
|
|
1156
1157
|
expectedSchedulerIds.add(schedulerId);
|
|
1157
|
-
await consumerQueue.upsertJobScheduler(schedulerId,
|
|
1158
|
+
await consumerQueue.upsertJobScheduler(schedulerId, cronRepeatOptions(jobDef.trigger), {
|
|
1158
1159
|
name: jobDef.perTenant ? `_perTenant:${name}` : name,
|
|
1159
1160
|
data: {},
|
|
1160
1161
|
// Queue-level defaultJobOptions (jobRetentionOpts) covers
|
|
@@ -33,6 +33,7 @@ export type PiiEventBackfillResult = {
|
|
|
33
33
|
readonly updatedEvents: number;
|
|
34
34
|
readonly encryptedFields: number;
|
|
35
35
|
readonly erasedFields: number;
|
|
36
|
+
readonly rekeyedFields: number;
|
|
36
37
|
readonly failures: readonly PiiBackfillFailure[];
|
|
37
38
|
readonly queuedTables: readonly string[];
|
|
38
39
|
readonly rebuild: PiiRebuildRun | null;
|
|
@@ -68,8 +68,14 @@ export function piiAnnotationEntries(registry) {
|
|
|
68
68
|
}
|
|
69
69
|
return entries.sort();
|
|
70
70
|
}
|
|
71
|
+
// Bump when the backfill gains a transform stage: completed states keyed by the
|
|
72
|
+
// old fingerprint must run one more full pass. Kept out of piiAnnotationEntries,
|
|
73
|
+
// whose emptiness gates no_pii.
|
|
74
|
+
const PII_BACKFILL_TRANSFORM_VERSION = "owner-rekey-v1";
|
|
71
75
|
export function piiAnnotationFingerprint(registry) {
|
|
72
|
-
return createHash("sha256")
|
|
76
|
+
return createHash("sha256")
|
|
77
|
+
.update([...piiAnnotationEntries(registry), PII_BACKFILL_TRANSFORM_VERSION].join("\n"))
|
|
78
|
+
.digest("hex");
|
|
73
79
|
}
|
|
74
80
|
async function queueRebuildTables(db, tableNames, migrationId) {
|
|
75
81
|
for (const tableName of tableNames) {
|
|
@@ -125,6 +131,7 @@ async function runOneBatch(ctx, progress, retryEventIds) {
|
|
|
125
131
|
...(retryEventIds && { eventIds: retryEventIds }),
|
|
126
132
|
batchSize: ctx.batchSize,
|
|
127
133
|
resolveOwnerFromProjection: true,
|
|
134
|
+
rekeyTenantKeyedOwnerFields: true,
|
|
128
135
|
scanCache: progress.scanCache,
|
|
129
136
|
});
|
|
130
137
|
const batchTouchedEventTypes = new Set(batch.touchedEventTypes);
|
|
@@ -151,6 +158,7 @@ function recordBatch(progress, step) {
|
|
|
151
158
|
progress.totals.updatedEvents += batch.updatedEvents;
|
|
152
159
|
progress.totals.encryptedFields += batch.encryptedFields;
|
|
153
160
|
progress.totals.erasedFields += batch.erasedFields;
|
|
161
|
+
progress.totals.rekeyedFields += batch.rekeyedFields;
|
|
154
162
|
for (const failure of batch.failures) {
|
|
155
163
|
// The catch-up overlap and the per-event retry can hit the same failed event twice in one run.
|
|
156
164
|
if (progress.failedIdsSeen.has(failure.eventId))
|
|
@@ -218,7 +226,13 @@ export async function runPiiEventBackfill(db, registry, options = {}) {
|
|
|
218
226
|
const progress = {
|
|
219
227
|
scanCursor,
|
|
220
228
|
scanCache: {},
|
|
221
|
-
totals: {
|
|
229
|
+
totals: {
|
|
230
|
+
scannedEvents: 0,
|
|
231
|
+
updatedEvents: 0,
|
|
232
|
+
encryptedFields: 0,
|
|
233
|
+
erasedFields: 0,
|
|
234
|
+
rekeyedFields: 0,
|
|
235
|
+
},
|
|
222
236
|
failures: [],
|
|
223
237
|
queuedTables: new Set(),
|
|
224
238
|
failedIdsSeen: new Set(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { deleteMany, fetchOne, updateMany } from "../db/query.js";
|
|
1
|
+
import { deleteMany, fetchOne, selectMany, updateMany } from "../db/query.js";
|
|
2
2
|
import { OnDeleteStrategies, SystemHookNames, SystemHookPriorities } from "../engine/constants.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isRecordLocked } from "../engine/screen-helpers.js";
|
|
4
|
+
import { ConflictError, FrameworkReasons, RecordLockedError } from "../errors/index.js";
|
|
4
5
|
export function createCascadeDeleteHook(registry, tables) {
|
|
5
6
|
return {
|
|
6
7
|
name: SystemHookNames.cascadeDelete,
|
|
@@ -47,6 +48,20 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
// The raw delete/update below bypasses the executor, so a locked child must
|
|
52
|
+
// block the parent delete here instead of changing with it. The error names
|
|
53
|
+
// the parent only: the caller may delete the parent without reading the child.
|
|
54
|
+
if (strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull) {
|
|
55
|
+
const childLock = registry.getEntity(relation.target)?.lockedWhen;
|
|
56
|
+
if (childLock !== undefined) {
|
|
57
|
+
const children = await selectMany(db, targetTable, {
|
|
58
|
+
[relation.foreignKey]: payload.id,
|
|
59
|
+
});
|
|
60
|
+
if (children.some((child) => isRecordLocked(childLock, child))) {
|
|
61
|
+
throw new RecordLockedError({ entityName, entityId: payload.id });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
50
65
|
if (strategy === OnDeleteStrategies.cascade) {
|
|
51
66
|
await deleteMany(db, targetTable, { [relation.foreignKey]: payload.id });
|
|
52
67
|
}
|
|
@@ -8,6 +8,9 @@ import { checkWriteFieldOwnership, maskWriteOnlyFields } from "../engine/field-a
|
|
|
8
8
|
import { defineTransitions, guardTransition } from "../engine/state-machine.js";
|
|
9
9
|
import { HookPhases } from "../engine/types/index.js";
|
|
10
10
|
import { AccessDeniedError, FrameworkReasons, InternalError, memberResolutionReadOnlyDenied, NotFoundError, ValidationError, writeFailure, } from "../errors/index.js";
|
|
11
|
+
import { assertFileRefsBelongToTenant } from "../files/assert-file-refs-belong-to-tenant.js";
|
|
12
|
+
import { bindPendingFileRefs } from "../files/bind-pending-file-refs.js";
|
|
13
|
+
import { disposeRemovedFileRefs } from "../files/dispose-removed-file-refs.js";
|
|
11
14
|
import { CACHE_SYNC_TOPICS } from "../redis/cache-sync-topics.js";
|
|
12
15
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
13
16
|
import { buildHandlerContext, CONFIG_WRITE_RESET_TYPE, CONFIG_WRITE_SET_TYPE, isMemberResolutionPrincipal, resolveDbSource, runHandlerInstrumented, TENANT_TIMEZONE_CONFIG_KEY, } from "./dispatch-shared.js";
|
|
@@ -111,6 +114,10 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
111
114
|
// keeps projection state consistent with what the hooks observe.
|
|
112
115
|
await runProjections(result, handlerContext.registry, runner);
|
|
113
116
|
if (result.kind === "save") {
|
|
117
|
+
const fileRefArgs = { registry: handlerContext.registry, result, user, db: handlerContext.db };
|
|
118
|
+
await assertFileRefsBelongToTenant(fileRefArgs);
|
|
119
|
+
await bindPendingFileRefs(fileRefArgs);
|
|
120
|
+
await disposeRemovedFileRefs(fileRefArgs);
|
|
114
121
|
await lifecycle.runPostSave(type, result, handlerContext, HookPhases.inTransaction);
|
|
115
122
|
afterCommitHooks.push(async () => {
|
|
116
123
|
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, undefined, undefined, undefined, true);
|
package/dist/schema-cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join, resolve as resolvePath } from "node:path";
|
|
12
12
|
import { configureBlindIndexKey, configurePiiSubjectKms, resolveKmsWiringAsync, } from "./crypto/index.js";
|
|
13
|
-
import { assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
|
|
13
|
+
import { assertUniqueTableNames, assertValidMigrationName, baselineMigrations, createDbConnection, diffReplayAgainstSnapshot, fetchAppliedMigrations, findCommentedDropTables, generateMigration, isRetiredFrameworkTable, loadMigrationsFromDir, loadSnapshotJson, readMigrationSqlTexts, rebuildTablesFromDiff, replayMigrationsDir, runMigrationsFromDir, tableExists, writeRebuildMarker, writeSnapshotJson, } from "./db/index.js";
|
|
14
14
|
import { validateBoot } from "./engine/boot-validator.js";
|
|
15
15
|
import { createRegistry } from "./engine/registry.js";
|
|
16
16
|
import { createEventsTable } from "./event-store/index.js";
|
|
@@ -29,10 +29,9 @@ async function loadSchemaModFromApp(schemaFile) {
|
|
|
29
29
|
if (!Array.isArray(mod.ENTITY_METAS)) {
|
|
30
30
|
throw new Error(`Schema file ${schemaFile} muss \`export const ENTITY_METAS: EntityTableMeta[]\` haben.`);
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
32
|
+
const entityMetas = mod.ENTITY_METAS;
|
|
33
|
+
assertUniqueTableNames(entityMetas);
|
|
34
|
+
return { entityMetas, features: mod.FEATURES };
|
|
36
35
|
}
|
|
37
36
|
function nextSequenceNumber(migrationsDir) {
|
|
38
37
|
if (!existsSync(migrationsDir))
|
|
@@ -134,7 +133,14 @@ export async function runSchemaCli(argv, appCwd, out, options = {}) {
|
|
|
134
133
|
out.err(" export const ENTITY_METAS: EntityTableMeta[] = [...]");
|
|
135
134
|
return 1;
|
|
136
135
|
}
|
|
137
|
-
|
|
136
|
+
let metas;
|
|
137
|
+
try {
|
|
138
|
+
({ entityMetas: metas } = await loadSchemaModFromApp(schemaFile));
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
out.err(` ${e instanceof Error ? e.message : String(e)}`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
138
144
|
const snapshotPath = join(migrationsDir, SNAPSHOT_FILENAME);
|
|
139
145
|
const prevSnapshot = existsSync(snapshotPath) ? loadSnapshotJson(snapshotPath) : null;
|
|
140
146
|
const result = generateMigration({
|
|
@@ -13,10 +13,9 @@ function quoteIdent(name) {
|
|
|
13
13
|
}
|
|
14
14
|
// Read-table state, forward-mapped from entity.fields (fieldName →
|
|
15
15
|
// toSnakeCase(fieldName)). SELECT * + forward-map instead of building an
|
|
16
|
-
// explicit column list: some field types (
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// are just skipped, matching what a `.created` event payload would carry.
|
|
16
|
+
// explicit column list: some field types (locatedTimestamp, money) emit
|
|
17
|
+
// companion columns under a different name — an explicit alias list breaks on
|
|
18
|
+
// those. Missing columns are just skipped, matching what a `.created` event payload would carry.
|
|
20
19
|
function rowToState(row, fieldNames) {
|
|
21
20
|
const state = {};
|
|
22
21
|
for (const fieldName of fieldNames) {
|
|
@@ -7,7 +7,7 @@ import type { ErrorDocsConfig } from "../errors/docs-url.js";
|
|
|
7
7
|
import { type JobRunner, type JobRunnerOptions } from "../jobs/index.js";
|
|
8
8
|
import type { Lifecycle } from "../lifecycle/index.js";
|
|
9
9
|
import { type ObservabilityProvider } from "../observability/index.js";
|
|
10
|
-
import type { Dispatcher, EventDispatcher } from "../pipeline/index.js";
|
|
10
|
+
import type { Dispatcher, EventDispatcher, SystemHooks } from "../pipeline/index.js";
|
|
11
11
|
import { type ClosableCacheSyncBus } from "../redis/cache-sync-bus.js";
|
|
12
12
|
import type { SearchAdapter } from "../search/types.js";
|
|
13
13
|
import { type EventCollector } from "./event-collector.js";
|
|
@@ -37,6 +37,8 @@ export type TestStackOptions = {
|
|
|
37
37
|
features: readonly FeatureDefinition[];
|
|
38
38
|
/** System hooks to wire up. Default: all (sse, search) */
|
|
39
39
|
systemHooks?: ("sse" | "search")[];
|
|
40
|
+
/** Pipeline system hooks handed to buildServer, e.g. the cascade-delete preDelete hook. None by default. */
|
|
41
|
+
lifecycleSystemHooks?: SystemHooks;
|
|
40
42
|
/** Search config per tenant — defaults to tenant 1 with all text fields */
|
|
41
43
|
searchConfig?: {
|
|
42
44
|
tenantId: TenantId;
|
package/dist/stack/test-stack.js
CHANGED
|
@@ -287,6 +287,7 @@ export async function setupTestStack(options) {
|
|
|
287
287
|
context: rateLimitFallback ? { ...appContext, rateLimit: rateLimitFallback } : appContext,
|
|
288
288
|
jwtSecret,
|
|
289
289
|
...(options.observability ? { observability: options.observability } : {}),
|
|
290
|
+
...(options.lifecycleSystemHooks && { systemHooks: options.lifecycleSystemHooks }),
|
|
290
291
|
dispatcherOptions: {
|
|
291
292
|
idempotency,
|
|
292
293
|
cacheSync,
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -4,12 +4,12 @@ export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedd
|
|
|
4
4
|
export type { ParsedRefTarget } from "../engine/parse-ref-target.js";
|
|
5
5
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
6
6
|
export type { FieldsOrGroupsSection, UnavailableSelectOption } from "../engine/screen-helpers.js";
|
|
7
|
-
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
7
|
+
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
8
8
|
export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefinition, FieldDefinition, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, SelectFieldDef, SelectOptionTone, TextFieldDef, } from "../engine/types/fields.js";
|
|
9
9
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
11
11
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
|
|
12
|
-
export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
12
|
+
export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
13
13
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
14
14
|
export type { TargetRef } from "../engine/types/target-ref.js";
|
|
15
15
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
export { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
|
|
23
23
|
export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedded-derived.js";
|
|
24
24
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
25
|
-
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
25
|
+
export { availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
26
26
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
27
27
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
28
28
|
export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.361.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>",
|
|
@@ -210,8 +210,8 @@
|
|
|
210
210
|
"./package.json": "./package.json"
|
|
211
211
|
},
|
|
212
212
|
"dependencies": {
|
|
213
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
214
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-http": "0.361.0",
|
|
214
|
+
"@cosmicdrift/kumiko-types": "0.361.0",
|
|
215
215
|
"bullmq": "^5.76.7",
|
|
216
216
|
"bun-types": "^1.3.13",
|
|
217
217
|
"hono": "^4.13.1",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
"zod": "^4.4.3"
|
|
228
228
|
},
|
|
229
229
|
"devDependencies": {
|
|
230
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
230
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.361.0",
|
|
231
231
|
"bun-types": "^1.3.13",
|
|
232
232
|
"pino-pretty": "^13.1.3"
|
|
233
233
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,139 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Boot check for edit field prefill",
|
|
6
|
+
"detail": "validateBoot allows today only on date fields, currentUser displayName and email only on text fields and id on text or reference fields."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.361.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Boot check for the entityEdit readOnly condition",
|
|
12
|
+
"detail": "validateBoot rejects a screen readOnly condition whose field is not a field of the screen's entity."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.361.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Boot check for embeddedList on edit fields",
|
|
18
|
+
"detail": "validateBoot requires the field to be an embedded list and every readOnlyCells entry to be a sub-field."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.361.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "Locked records reject executor writes",
|
|
24
|
+
"detail": "The event-store executor checks entity.lockedWhen against the loaded row in update and delete and fails with RecordLockedError (409, code record_locked, i18n key errors.recordLocked); a missing condition field counts as locked. Create, restore, forget, framework-system-user writes that name a writePurpose and the write that sets the lock pass. validateBoot rejects a lockedWhen field that is unknown, personal, encrypted or not a plain scalar type."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.361.0",
|
|
28
|
+
"type": "breaking",
|
|
29
|
+
"title": "POST /files checks write access to the target record",
|
|
30
|
+
"detail": "createRecordWriteCheck loads the stored row tenant-scoped and applies update-handler access, entity access.write, field write access and lockedWhen before the upload is stored. Results map to 404, 403 and 409 record_locked. Uploads with an entityId and no check wired are refused.",
|
|
31
|
+
"migration": "Grant the uploader the entity's update right for records they attach files to, or upload without an entityId and bind the file on save. Entities without a generic :update handler no longer accept uploads with an entityId. Code that used entityId as a free slot without a stored record must create the record first."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.361.0",
|
|
35
|
+
"type": "breaking",
|
|
36
|
+
"title": "Unbound uploads are private to the uploader (security)",
|
|
37
|
+
"detail": "The default file access guard denies every operation on a file with no entityType or no entityId unless the caller is the uploader or has a role in privilegedRoles; the file routes answer 404 as for any denied file. Before, files without entityType were readable by the whole tenant and pending uploads until a record claimed them. bindPendingFileRefs now also runs for updates and binds the file ids named in the update's changes for file, image, files and images fields (same entityType and fieldName, entityId null, insertedById = caller), so createFileRefFromBytes with attachTo but no entityId plus a following update write attaches the file to the record.",
|
|
38
|
+
"migration": "Code that shows a file without entityType to other users (tenant logo, avatar, mail attachment, import preview, AI-agent output) must upload it with entityType and fieldName of a file field on a record and save the record, so readers of that record can read it; or keep it uploader-only, run the read as a role in privilegedRoles, or set a custom accessGuard in the files feature options. Apps that set accessGuard keep their own decision. Code that reads another user's pending upload before the record is saved must bind the file first or run as the uploader. Known case: solon's document upload (`document/web/upload-file.ts`) posts without entityType and the later document record (created by the ingest job, not the uploader) stores the id in its `fileId` field, so other users no longer read the original; solon must upload with entityType and fieldName of `fileId` and bind the file when the record is created, because a different creator cannot claim the pending upload."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"version": "0.361.0",
|
|
42
|
+
"type": "fix",
|
|
43
|
+
"title": "Personal file/image fields are no longer encrypted",
|
|
44
|
+
"detail": "collectPiiSubjectFields still lists file, image, files and images fields with a personal annotation (forget and retention use it), but the new collectPiiEncryptedFields leaves them out and every encrypt, decrypt, backfill and store-gate path now uses it. The column holds only the fileRef uuid; the binary is erased by the file-ref user-data hook."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"version": "0.361.0",
|
|
48
|
+
"type": "fix",
|
|
49
|
+
"title": "Personal fields are re-keyed to the new owner on owner change",
|
|
50
|
+
"detail": "On absent to X and X to Y, the executor writes the personal fields (personal: { of: ownerField }) of the row under the new owner's key into the update event, so the projection, a rebuild and every event from the transition on agree and only the current owner's forget decides erasure. As a result <entity>.updated changes can contain personal fields the caller did not send. X to absent keeps the values under X so the last owner's forget still erases them. The created event from before an owner existed stays tenant-keyed in the immutable log. The new backfill option rekeyTenantKeyedOwnerFields moves such tenant-keyed values under the owner the projection or payload names (never via whenAbsent, a projection row of another tenant is ignored, other tenants' keys are not touched, a forgotten owner yields the erased sentinel). The boot PII backfill passes it, and its fingerprint now includes a transform version, so each deployment runs one full pass and afterwards only catch-up passes near the cursor. A created event whose owner transition happens after that full pass is not rescanned and stays tenant-keyed until someone runs backfillEventPiiEncryption(db, registry, { resolveOwnerFromProjection: true, rekeyTenantKeyedOwnerFields: true }) followed by a projection rebuild, or the transform version is bumped.",
|
|
51
|
+
"migration": "No consumer code change. The boot backfill runs in the background (startPiiEventBackfillOnBoot, it does not block readiness, KUMIKO_SKIP_PII_BACKFILL=1 turns it off). On the first boot after the bump it does one full scan of kumiko_events and rebuilds the affected projections. Consumers reading updated.changes must not treat a personal field there as a user edit when the owner field changed in the same event."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"version": "0.361.0",
|
|
55
|
+
"type": "breaking",
|
|
56
|
+
"title": "Plural file fields have a projection column",
|
|
57
|
+
"detail": "files and images fields get a jsonb NOT NULL DEFAULT '[]' column on the entity table (entity-table-meta, buildEntityTable row type readonly string[]) and applyEntityEvent no longer strips them from updated events. Detail and list queries return the ordered id arrays; before, they never carried the field, so an edit form of a saved record showed no files. kumiko schema generate emits an in-place ADD COLUMN for existing tables and no .rebuild.json, so existing rows hold [] until their projection is rebuilt from the events.",
|
|
58
|
+
"migration": "Run kumiko schema generate and apply the migration. Existing rows keep [] until the entity projection is rebuilt, so add a rebuild marker before applying: create <NNNN_migration-name>.rebuild.json next to the generated SQL with {\"version\":1,\"tables\":[\"<table name from the ALTER TABLE statement>\"]}; kumiko schema apply then rebuilds the projection. Without the marker, call enqueueProjectionRebuild(\"<feature>:projection:<entity>-entity\", { db, registry, jobRunner }) from @cosmicdrift/kumiko-framework/migrations once per entity. Known consumers: solon protocol (attachmentFileIds) and the Wartungs-Demo (photos). Code that read the ids of a plural file field from the event log or from file_refs can read the row field instead."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"version": "0.361.0",
|
|
62
|
+
"type": "improvement",
|
|
63
|
+
"title": "Files removed from a record are deleted",
|
|
64
|
+
"detail": "disposeRemovedFileRefs runs next to bindPendingFileRefs for every save that is not a create. For each file field named in changes it soft-deletes the fileRefs present in previous but not in the saved row, when entityType, entityId and fieldName match this record. The binary is removed by the soft-delete cleanup job after retention, or by forget."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"version": "0.361.0",
|
|
68
|
+
"type": "breaking",
|
|
69
|
+
"title": "Schema generate rejects a table listed twice in ENTITY_METAS",
|
|
70
|
+
"detail": "When ENTITY_METAS contained the same tableName twice, the migration diff kept only the last definition and compared it against the previous snapshot entry, so a new index was written to .snapshot.json but no CREATE INDEX reached the migration SQL. kumiko-schema generate, kumiko-schema validate and programmatic generateMigration now throw (generate and validate exit 1 with a message naming every duplicated table) before any file is written.",
|
|
71
|
+
"migration": "Remove hand-added metas for tables a feature already registers from kumiko/schema.ts. Known: phronexsis kumiko/schema.ts appends IN_APP_MESSAGES_META next to collectTableMetas(FEATURES); drop it (and its comment), then run kumiko-schema generate — the existing in_app_messages_tenant_user_created_idx comes out as CREATE INDEX IF NOT EXISTS (no-op) and the two legacy indexes as commented review drops. The duplicate in_app_messages entry in .snapshot.json disappears with that generate."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"version": "0.361.0",
|
|
75
|
+
"type": "fix",
|
|
76
|
+
"title": "Soft-delete cleanup removes the bytes of expired fileRefs",
|
|
77
|
+
"detail": "softDeleteCleanupJob handles the fileRef entity through purgeExpiredFileRefsWithBytes, which deletes the original and its derivatives via the file provider and then only the rows whose bytes are gone. Failures are logged and retried on the next run."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"version": "0.361.0",
|
|
81
|
+
"type": "improvement",
|
|
82
|
+
"title": "setupTestStack option lifecycleSystemHooks",
|
|
83
|
+
"detail": "Passes the given SystemHooks to buildServer, so tests can exercise pipeline lifecycle hooks like createCascadeDeleteHook through the real HTTP stack."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"version": "0.361.0",
|
|
87
|
+
"type": "breaking",
|
|
88
|
+
"title": "Writes with file ids from another tenant are rejected",
|
|
89
|
+
"detail": "A create or update that puts a file id into a file or image field now fails with a ValidationError (code invalid_element, path = field name) when the id is not a UUID, does not exist, or belongs to another tenant. That includes files stored under the SYSTEM tenant. On update only the changed fields are checked. The check runs for every write that returns a save result, inside the transaction, before pending file refs are bound. System users created for a tenant pass with that tenant's own files.",
|
|
90
|
+
"migration": "Check seeds, imports and handlers that copy file ids across tenants or reference files uploaded as SYSTEM. Upload the file in the target tenant (or copy it there) before writing its id. A custom handler that writes through an executor directly, without returning a save result, is not covered and needs its own check."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"version": "0.360.0",
|
|
94
|
+
"type": "breaking",
|
|
95
|
+
"title": "Secure host-only session cookies use the __Host- prefix",
|
|
96
|
+
"detail": "Auth, csrf and tenant cookies are named __Host-kumiko_auth, __Host-kumiko_csrf and __Host-kumiko_tenant when they are written Secure and without a Domain (the tenant cookie is always host-only). With auth.cookieDomain or without Secure the auth and csrf names stay kumiko_auth and kumiko_csrf. New auth option secureCookies forces Secure (default NODE_ENV === \"production\"). Login deletes the legacy unprefixed host-only cookies, logout and deleteTenantCookie delete the prefixed and the legacy names. The server reads only the resolved names, so a request that carries only the legacy kumiko_auth is anonymous. AUTH_COOKIE_NAME, CSRF_COOKIE_NAME and TENANT_COOKIE_NAME are removed from the framework and dispatcher-live; the base names and sessionCookieName() are exported from @cosmicdrift/kumiko-types/session-cookies and @cosmicdrift/kumiko-framework/api, getSessionCookies(c) returns the resolved names of a request. The testing package's CSRF_COOKIE_NAME stays and resolves to the unprefixed name (e2e servers run without Secure).",
|
|
97
|
+
"migration": "Every user logs in once after the deploy, because the old kumiko_auth cookie is no longer read. Custom clients, proxies or scripts that read or forward the cookies by literal name break: read the names through sessionCookieName() / resolveSessionCookieNames() (types package) or getSessionCookies(c) on the server, and handle both the prefixed and the plain name in browser code (readCsrfToken() already does). Replace imports of AUTH_COOKIE_NAME, CSRF_COOKIE_NAME and TENANT_COOKIE_NAME with AUTH_COOKIE_BASE, CSRF_COOKIE_BASE and TENANT_COOKIE_BASE plus sessionCookieName(); e2e helpers that imported CSRF_COOKIE_NAME from @cosmicdrift/kumiko-dispatcher-live import it from @cosmicdrift/kumiko-testing/e2e instead (known: solon e2e/_helpers/api.ts and e2e/session-ended.spec.ts, kumiko-enterprise ai-agent-live-transcription.integration.test.ts uses AUTH_COOKIE_NAME). A reverse proxy that strips or rewrites cookie names must pass the __Host- names unchanged. Staging or preview deployments over plain http need no change (Secure is off there); set auth.secureCookies (also accepted by runProdApp) explicitly when TLS terminates before the app and NODE_ENV is not \"production\". A standalone createAuthRoutes mount outside buildServer must also mount sessionCookiesMiddleware with the same resolved names, otherwise the readers fall back to the domain-less default."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"version": "0.360.0",
|
|
101
|
+
"type": "improvement",
|
|
102
|
+
"title": "Duplicate session cookie names end the cookie session",
|
|
103
|
+
"detail": "When the Cookie header carries the resolved auth or csrf cookie name more than once, authMiddleware ignores the cookie transport and expires every variant (prefixed host-only, legacy host-only, the cookieDomain variant and retired domains). With anonymousAccess the request continues as anonymous, otherwise it gets 401 missing_token; a Bearer token on the same request still authenticates. A prefixed and a legacy name side by side are not a duplicate. The cookie clearing moved from the auth routes into clearSessionCookies in session-cookies.ts; ResolvedSessionCookies carries domain and retiredDomains."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"version": "0.360.0",
|
|
107
|
+
"type": "breaking",
|
|
108
|
+
"title": "Origin guard defaults on when the public app URL is known",
|
|
109
|
+
"detail": "New auth option publicOrigin. Without allowedOrigins, cookieDomain and unsafeSkipOriginCheck, buildServer mounts the Origin guard for cookie-authenticated state-changing /api requests and admits publicOrigin plus the request's own host (Host header). runProdApp sets publicOrigin from auth.publicOrigin, then auth.mail.baseUrl, then the APP_BASE_URL env (origin only, path dropped); with none of them in production it logs a warning that the Origin guard is off and boots anyway. allowedOrigins keeps its strict meaning, and the WebSocket check and the anonymous downgrade still use only allowedOrigins.",
|
|
110
|
+
"migration": "Cookie-authenticated POST, PUT, PATCH or DELETE requests from a page on another origin than the public app URL and than the API host itself now get 403 origin_not_allowed in runProdApp deployments that set auth.mail.baseUrl or APP_BASE_URL. A web client served from a different host than the API (for example a sibling subdomain) must list its origin in auth.allowedOrigins. Same-host setups, custom domains served by the same server and Bearer clients need no change."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"version": "0.360.0",
|
|
114
|
+
"type": "improvement",
|
|
115
|
+
"title": "createFileRefFromBytes creates FileRefs from handler- or job-produced bytes",
|
|
116
|
+
"detail": "createFileRefFromBytes({ db, files, registry, user }, { fileName, mimeType, data, attachTo? }) shares one pipeline with POST /api/files, so identifier checks, field-level maxSize/accept, content sniffing, MIME resolution, the tenant-prefixed storage key and the fileRef.created event are identical. The tenant always comes from ctx.user. A rejected file returns a 422 write failure with reason file_rejected and the upload error string in details.error. The FileRef's insertedById is the passed user, so the default access guard lets only that user and privileged roles read files attached to an entity."
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"version": "0.360.0",
|
|
120
|
+
"type": "improvement",
|
|
121
|
+
"title": "Cron jobs with an IANA time zone, images fields with capture",
|
|
122
|
+
"detail": "A cron trigger accepts tz (IANA name; without it the server process zone applies) and is scheduled in that zone, DST included. r.job() throws at boot for an invalid zone. createImagesField accepts capture (\"environment\" | \"user\") like createImageField, and the edit view model carries it for images fields."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"version": "0.360.0",
|
|
126
|
+
"type": "breaking",
|
|
127
|
+
"title": "Uploads from an update form are attached to the record",
|
|
128
|
+
"detail": "computeEditViewModel accepts entityId (the record being edited, update mode only) and sets it on the hints of file, image, files and images fields; RenderEdit passes the id it was mounted with and the file input sends it as the entityId multipart part of POST /api/files. Create forms send no entityId; after a successful create the write pipeline binds the caller's own pending uploads named in the record's file fields (same entityType and fieldName, entityId null, insertedById = caller) to the new record via a fileRef.updated event in the same transaction, for single and plural file fields alike, when the files feature (fileRef entity) is mounted. Behaviour change: such files are now attached to the record. The default file access guard allows read on an attached file to the uploader, privilegedRoles (default Admin/SystemAdmin) and everyone whose run of the entity's <entity>:detail query returns the record; delete stays uploader and privilegedRoles only. A qr-overlay variant of an image field no longer throws for a file uploaded from an update or create form. Write-form sections send no entityId. EmbeddedListInput has a new required actionsLabel prop.",
|
|
129
|
+
"migration": "Files attached through forms now follow the record's read access instead of being tenant-wide: anyone who cannot read the record no longer reads the file, anyone who can read it does. Check image, images, file and files fields whose files must be visible beyond the record's readers (for example an avatar shown in lists to users without detail access) and add those roles to privilegedRoles or set accessGuard in the files feature options. An entity without a detail query falls back to uploader plus privilegedRoles. Files uploaded before the update keep their old attachment. Custom primitive sets that implement EmbeddedListInput accept the new actionsLabel prop and render it as the actions column header; custom file/image Input primitives forward entityId into the upload and offer a remove control that calls onChange(null)."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"version": "0.360.0",
|
|
133
|
+
"type": "improvement",
|
|
134
|
+
"title": "images and files fields get an edit widget; required is allowed",
|
|
135
|
+
"detail": "NO_WIDGET_FIELD_TYPES now holds only jsonb and embedded. required true on an images or files field passes boot validation and the client-side check treats an empty array as missing. The edit view model carries maxCount for files and images fields."
|
|
136
|
+
},
|
|
2
137
|
{
|
|
3
138
|
"version": "0.359.0",
|
|
4
139
|
"type": "improvement",
|