@cosmicdrift/kumiko-framework 0.336.1 → 0.337.1
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/auth-middleware.js +43 -19
- package/dist/api/auth-routes.d.ts +1 -0
- package/dist/api/auth-routes.js +20 -0
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +4 -5
- package/dist/api/server.js +3 -3
- package/dist/api/sse-route.js +14 -1
- package/dist/bun-db/query.js +6 -4
- package/dist/crypto/kek-source.js +1 -0
- package/dist/db/blind-index-cleanup.js +1 -1
- package/dist/db/event-store-executor-read.js +15 -4
- package/dist/db/queries/event-consumer.d.ts +2 -2
- package/dist/db/queries/event-consumer.js +5 -4
- package/dist/db/queries/event-store.js +6 -6
- package/dist/db/queries/projection-rebuild.js +4 -4
- package/dist/db/queries/seed-context.js +6 -6
- package/dist/derivatives/derivatives-context.js +5 -2
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +13 -0
- package/dist/engine/boot-validator/screens.js +12 -0
- package/dist/engine/build-config-feature-schema.js +4 -0
- package/dist/engine/define-feature.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/jobs-routes.js +6 -2
- package/dist/engine/feature-ast/extractors/round3.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round3.js +11 -0
- package/dist/engine/feature-ast/extractors/shared.js +1 -3
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +3 -0
- package/dist/engine/feature-ast/patch.js +8 -11
- package/dist/engine/feature-ast/patcher.d.ts +6 -1
- package/dist/engine/feature-ast/patcher.js +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +590 -1
- package/dist/engine/feature-ast/pattern-change-schema.js +17 -2
- package/dist/engine/feature-ast/patterns.d.ts +8 -2
- package/dist/engine/feature-ast/patterns.js +1 -0
- package/dist/engine/feature-ast/render.js +9 -0
- package/dist/engine/feature-builder-state.d.ts +2 -1
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-config-events-jobs.d.ts +4 -1
- package/dist/engine/feature-config-events-jobs.js +20 -0
- package/dist/engine/feature-ui-extensions.js +15 -21
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +47 -4
- package/dist/engine/registry-facade.js +7 -0
- package/dist/engine/registry-ingest.js +7 -0
- package/dist/engine/registry-state.d.ts +2 -1
- package/dist/engine/registry-state.js +1 -0
- package/dist/engine/registry-validate.d.ts +1 -1
- package/dist/engine/registry-validate.js +39 -17
- package/dist/engine/registry.js +2 -2
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/types/index.js +1 -1
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/errors/classes.d.ts +5 -0
- package/dist/errors/classes.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/i18n/required-surface-keys.js +3 -5
- package/dist/migrations/pending-rebuilds.d.ts +2 -0
- package/dist/migrations/pending-rebuilds.js +3 -0
- package/dist/pipeline/dispatch-query.js +3 -5
- package/dist/pipeline/dispatch-stream.js +2 -4
- package/dist/pipeline/dispatch-write.js +2 -4
- package/dist/pipeline/event-dispatcher-delivery.d.ts +0 -1
- package/dist/pipeline/event-dispatcher-delivery.js +1 -8
- package/dist/pipeline/event-dispatcher.js +16 -25
- package/dist/pipeline/handler-access-error.d.ts +3 -0
- package/dist/pipeline/handler-access-error.js +17 -0
- package/dist/pipeline/irreversible-operation-gate.js +9 -3
- package/dist/pipeline/member-read-only-transaction.js +8 -3
- package/dist/pipeline/system-hooks.js +7 -1
- package/dist/pipeline/system-identity-switch.js +1 -1
- package/dist/search/meilisearch-adapter.js +0 -2
- package/dist/stack/push-entity-projection-tables.js +5 -3
- package/dist/stack/redis.d.ts +1 -0
- package/dist/stack/redis.js +11 -2
- package/dist/stack/table-helpers.d.ts +3 -1
- package/dist/stack/table-helpers.js +9 -7
- package/dist/stack/test-stack.js +1 -7
- package/dist/testing/handler-context.js +3 -6
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +29 -2
- package/package.json +4 -4
- package/src/changes.json +92 -0
- package/src/scripts/codemod/collect-source-files.ts +48 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +3 -23
- package/src/scripts/codemod/migrate-db-raw.ts +6 -23
- package/src/scripts/codemod/migrate-open-to-all.ts +4 -24
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +47 -3
package/dist/errors/classes.js
CHANGED
|
@@ -42,6 +42,21 @@ export class AccessDeniedError extends KumikoError {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
// A caller without a signed-in principal (the synthesised anonymousAccess
|
|
46
|
+
// user) hit a handler that needs one. 401, not 403: an expired auth cookie is
|
|
47
|
+
// dropped by the browser, so this is how a lapsed session reaches the server.
|
|
48
|
+
export class UnauthenticatedError extends KumikoError {
|
|
49
|
+
code = "unauthenticated";
|
|
50
|
+
httpStatus = 401;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
super({
|
|
53
|
+
message: opts?.message ?? "this handler requires a signed-in user",
|
|
54
|
+
i18nKey: "auth.errors.missingToken",
|
|
55
|
+
...(opts?.details !== undefined && { details: opts.details }),
|
|
56
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
export class NotFoundError extends KumikoError {
|
|
46
61
|
code = "not_found";
|
|
47
62
|
httpStatus = 404;
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { FeatureDisabledDetails, FieldIssue, NotFoundDetails, PreconditionFailedDetails, RateLimitDetails, UnconfiguredDetails, UniqueViolationDetails, UnprocessableOpts, ValidationDetails, VersionConflictDetails, } from "./classes.js";
|
|
2
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
3
3
|
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error.js";
|
|
4
4
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
5
5
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
1
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
2
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
3
3
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
4
4
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
@@ -144,6 +144,9 @@ function pushRelatedListSectionKeys(out, section) {
|
|
|
144
144
|
}
|
|
145
145
|
for (const action of section.rowActions ?? [])
|
|
146
146
|
pushRowActionKeys(out, action);
|
|
147
|
+
pushKey(out, section.groupBy?.label);
|
|
148
|
+
for (const label of Object.values(section.groupBy?.labels ?? {}))
|
|
149
|
+
pushKey(out, label);
|
|
147
150
|
}
|
|
148
151
|
function pushExpandableRowKeys(out, section) {
|
|
149
152
|
pushRelatedListSectionKeys(out, section);
|
|
@@ -330,11 +333,6 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
330
333
|
}
|
|
331
334
|
if (section.kind === "relatedList") {
|
|
332
335
|
pushRelatedListSectionKeys(out, section);
|
|
333
|
-
if (section.groupBy !== undefined) {
|
|
334
|
-
pushKey(out, section.groupBy.label);
|
|
335
|
-
for (const label of Object.values(section.groupBy.labels ?? {}))
|
|
336
|
-
pushKey(out, label);
|
|
337
|
-
}
|
|
338
336
|
continue;
|
|
339
337
|
}
|
|
340
338
|
if (isWriteFormEditSection(section)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DbConnection } from "../db/connection.js";
|
|
2
2
|
import type { Registry } from "../engine/types/index.js";
|
|
3
3
|
import type { JobRunner } from "../jobs/index.js";
|
|
4
|
+
import type { Logger } from "../logging/types.js";
|
|
4
5
|
import { type RebuildResult } from "../pipeline/index.js";
|
|
5
6
|
export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
|
|
6
7
|
export declare function createPendingRebuildsTable(db: DbConnection): Promise<void>;
|
|
@@ -56,6 +57,7 @@ export declare function rebuildProjectionOrMultiStream(name: string, deps: {
|
|
|
56
57
|
readonly errorPolicy?: {
|
|
57
58
|
readonly skipApplyErrors?: boolean;
|
|
58
59
|
};
|
|
60
|
+
readonly logger?: Pick<Logger, "error"> & Partial<Pick<Logger, "warn" | "debug">>;
|
|
59
61
|
}): Promise<RebuildResult>;
|
|
60
62
|
/** Logs a finished queue drain and throws when any projection failed; the
|
|
61
63
|
* failed tables stay queued. Shared by every `schema apply` entry point so the
|
|
@@ -72,6 +72,9 @@ export async function clearPendingRebuilds(db, rows) {
|
|
|
72
72
|
export async function rebuildProjectionOrMultiStream(name, deps) {
|
|
73
73
|
const { db, registry, signal, errorPolicy } = deps;
|
|
74
74
|
if (registry.getAllMultiStreamProjections().has(name)) {
|
|
75
|
+
if (errorPolicy?.skipApplyErrors === true) {
|
|
76
|
+
createFallbackLogger("migrations:pending-rebuilds", deps.logger).warn(`skipApplyErrors ignored for multi-stream projection "${name}"; declare msp.errorMode.rebuild instead`, { projection: name });
|
|
77
|
+
}
|
|
75
78
|
return rebuildMultiStreamProjection(name, {
|
|
76
79
|
db,
|
|
77
80
|
registry,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hasAccess } from "../engine/access.js";
|
|
2
2
|
import { filterReadFields } from "../engine/field-access.js";
|
|
3
|
-
import {
|
|
3
|
+
import { NotFoundError, validationErrorFromZod } from "../errors/index.js";
|
|
4
4
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
5
5
|
import { buildHandlerContext, enforceRateLimit, ensureFeatureEnabled, isMemberResolutionPrincipal, runHandlerInstrumented, } from "./dispatch-shared.js";
|
|
6
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
6
7
|
import { runInMemberReadOnlyTransaction } from "./member-read-only-transaction.js";
|
|
7
8
|
// Standalone query execution — used by the public dispatcher.query() and
|
|
8
9
|
// by ctx.query/ctx.queryAs inside handlers. Runs the handler, applies
|
|
@@ -34,10 +35,7 @@ async function executeQueryInner(ctx, type, payload, user, origin, tx) {
|
|
|
34
35
|
// so in normal boots this branch shouldn't fire — the guard is belt-and-
|
|
35
36
|
// suspenders in case a handler sneaks through (e.g. runtime injection).
|
|
36
37
|
if (!hasAccess(user, handler.access)) {
|
|
37
|
-
throw
|
|
38
|
-
message: `access denied for ${type}`,
|
|
39
|
-
details: { handler: type },
|
|
40
|
-
});
|
|
38
|
+
throw handlerAccessError(user, type);
|
|
41
39
|
}
|
|
42
40
|
const parsed = handler.schema.safeParse(payload);
|
|
43
41
|
if (!parsed.success) {
|
|
@@ -3,6 +3,7 @@ import { hasAccess } from "../engine/access.js";
|
|
|
3
3
|
import { AccessDeniedError, memberResolutionReadOnlyDenied, NotFoundError, validationErrorFromZod, } from "../errors/index.js";
|
|
4
4
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
5
5
|
import { buildHandlerContext, enforceRateLimit, ensureFeatureEnabled, isMemberResolutionPrincipal, runStreamInstrumented, } from "./dispatch-shared.js";
|
|
6
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
6
7
|
// Standalone stream execution — used by the public dispatcher.stream().
|
|
7
8
|
// Chunk-by-chunk analog of executeQuery: same gate order (feature → rate-
|
|
8
9
|
// limit → access → validation → handler), but yields incrementally instead
|
|
@@ -25,10 +26,7 @@ async function* executeStreamInner(ctx, type, payload, user, origin) {
|
|
|
25
26
|
await ensureFeatureEnabled(ctx, type, user.tenantId);
|
|
26
27
|
await enforceRateLimit(ctx, handler.rateLimit, type, user, registry.isHandlerSystemScoped(type));
|
|
27
28
|
if (!hasAccess(user, handler.access)) {
|
|
28
|
-
throw
|
|
29
|
-
message: `access denied for ${type}`,
|
|
30
|
-
details: { handler: type },
|
|
31
|
-
});
|
|
29
|
+
throw handlerAccessError(user, type);
|
|
32
30
|
}
|
|
33
31
|
const parsed = handler.schema.safeParse(payload);
|
|
34
32
|
if (!parsed.success) {
|
|
@@ -13,6 +13,7 @@ import { AccessDeniedError, FrameworkReasons, InternalError, isKumikoError, memb
|
|
|
13
13
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
14
14
|
import { buildHandlerContext, CONFIG_WRITE_RESET_TYPE, CONFIG_WRITE_SET_TYPE, checkFeatureEnabled, enforceRateLimit, isMemberResolutionPrincipal, resolveDbSource, runHandlerInstrumented, TENANT_TIMEZONE_CONFIG_KEY, } from "./dispatch-shared.js";
|
|
15
15
|
import { describeShape, extractNestedSpecs, isLifecycleResult, isWriteResultShape, prefixValidationPath, wrapToKumiko, } from "./dispatcher-utils.js";
|
|
16
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
16
17
|
import { runProjections } from "./projections-runner.js";
|
|
17
18
|
function getTable(ctx, entityName) {
|
|
18
19
|
const { registry, tableCache } = ctx;
|
|
@@ -322,10 +323,7 @@ async function executeWriteInner(ctx, type, payload, user, origin, tx, afterComm
|
|
|
322
323
|
// so in normal boots this branch shouldn't fire — the guard is belt-and-
|
|
323
324
|
// suspenders in case a handler sneaks through (e.g. runtime injection).
|
|
324
325
|
if (!hasAccess(user, handler.access)) {
|
|
325
|
-
return writeFailure(
|
|
326
|
-
message: `access denied for ${type}`,
|
|
327
|
-
details: { handler: type },
|
|
328
|
-
}));
|
|
326
|
+
return writeFailure(handlerAccessError(user, type));
|
|
329
327
|
}
|
|
330
328
|
const parsed = handler.schema.safeParse(payload);
|
|
331
329
|
if (!parsed.success) {
|
|
@@ -67,5 +67,4 @@ export type PersistedConsumerOutcome = DeliveryOutcome & {
|
|
|
67
67
|
readonly pendingGaps: readonly PendingGapEntry[];
|
|
68
68
|
};
|
|
69
69
|
export declare function persistConsumerOutcome(tx: DbTx, name: string, instanceId: string, outcome: PersistedConsumerOutcome): Promise<void>;
|
|
70
|
-
export declare function persistConsumerPassFailure(tx: DbTx, name: string, instanceId: string, errorMessage: string): Promise<void>;
|
|
71
70
|
export declare function emitLagFromTx(tx: DbTx, consumerName: string, instanceId: string, cursor: bigint, meter: Meter): Promise<void>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// resolve against (same #1438 dual-package-hazard pattern as event-store.ts).
|
|
5
5
|
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
6
6
|
import { requestContext } from "../api/request-context.js";
|
|
7
|
-
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer,
|
|
7
|
+
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer, selectConsumerForUpdate, selectConsumerForUpdateSkipLocked, selectProvablyIdleConsumerPairs, updateConsumerDeliveryOutcome, } from "../db/queries/event-consumer.js";
|
|
8
8
|
import { selectEventsHeadId, selectPendingAndNewEventRows, } from "../db/queries/event-store.js";
|
|
9
9
|
import { coerceRow, extractTableInfo } from "../db/query.js";
|
|
10
10
|
import { qnScope } from "../engine/qualified-name.js";
|
|
@@ -319,13 +319,6 @@ function reportSkippedEvent(consumer, eventId, e, errMessage, context) {
|
|
|
319
319
|
export async function persistConsumerOutcome(tx, name, instanceId, outcome) {
|
|
320
320
|
await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
|
|
321
321
|
}
|
|
322
|
-
// Best-effort record of a pass that threw before persistConsumerOutcome
|
|
323
|
-
// could run (event-dispatcher.ts's processConsumer catch). The caller opens
|
|
324
|
-
// its OWN transaction for this — the one that rolled back never committed
|
|
325
|
-
// anything, including this write, if it happened inside the same tx.
|
|
326
|
-
export async function persistConsumerPassFailure(tx, name, instanceId, errorMessage) {
|
|
327
|
-
await recordConsumerPassFailure(tx, name, instanceId, errorMessage);
|
|
328
|
-
}
|
|
329
322
|
// Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
|
|
330
323
|
// consistent with the cursor we just advanced to. `MAX(id)` on the events
|
|
331
324
|
// table is an O(1) reverse-index scan — cheap even under load.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { selectSnapshotXmax, selectSnapshotXmin } from "../db/queries/event-consumer.js";
|
|
1
|
+
import { recordConsumerPassFailure, selectSnapshotXmax, selectSnapshotXmin, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
3
3
|
import { EVENTS_PUBSUB_CHANNEL } from "../event-store/index.js";
|
|
4
4
|
import { emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, getFallbackMeter, getFallbackTracer, } from "../observability/index.js";
|
|
5
5
|
import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
|
|
6
|
-
import { acquireConsumerState, consumerInstanceId, deliverEvents, emitLagFromTx, fetchPendingEvents, markProcessing, persistConsumerOutcome,
|
|
6
|
+
import { acquireConsumerState, consumerInstanceId, deliverEvents, emitLagFromTx, fetchPendingEvents, markProcessing, persistConsumerOutcome, preRegisterConsumers, selectIdleConsumerKeys, } from "./event-dispatcher-delivery.js";
|
|
7
7
|
import { partitionBurntGaps, splitRangeExcludingIds, toIdRanges } from "./pending-gap-ranges.js";
|
|
8
8
|
const DEFAULT_BATCH_SIZE = 200;
|
|
9
9
|
const DEFAULT_POLL_MS = 100;
|
|
@@ -35,6 +35,15 @@ export function createEventDispatcher(options) {
|
|
|
35
35
|
}
|
|
36
36
|
const tracer = options.tracer ?? getFallbackTracer();
|
|
37
37
|
const meter = options.meter ?? getFallbackMeter();
|
|
38
|
+
// A standalone dispatcher has no logger on its context; console.error keeps the failure in pod logs.
|
|
39
|
+
const logDispatcherError = (message) => {
|
|
40
|
+
if (context.log) {
|
|
41
|
+
context.log.error(message);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.error(message);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
38
47
|
// Tracks which (consumer, instanceId) pairs already fired
|
|
39
48
|
// kumiko_event_consumer_rearm_exhausted_total, so a consumer stuck dead
|
|
40
49
|
// across many poll passes emits the ops-signal once, not every pass
|
|
@@ -220,7 +229,7 @@ export function createEventDispatcher(options) {
|
|
|
220
229
|
//
|
|
221
230
|
// A failure here (e.g. a transient DB blip) must not abort the whole
|
|
222
231
|
// pass — timer/LISTEN callers swallow doPass()'s rejection silently
|
|
223
|
-
// (see start()), which would mean no log, no
|
|
232
|
+
// (see start()), which would mean no log, no recordConsumerPassFailure,
|
|
224
233
|
// and no backoff for every consumer this tick. Falling back to "nothing
|
|
225
234
|
// proven idle" instead routes every consumer through its normal
|
|
226
235
|
// acquireConsumerState path, whose own try/catch already covers this.
|
|
@@ -254,12 +263,7 @@ export function createEventDispatcher(options) {
|
|
|
254
263
|
emitEventDispatcherListenConnected(meter, false);
|
|
255
264
|
const msg = e instanceof Error ? e.message : String(e);
|
|
256
265
|
const logMsg = `[event-dispatcher] idle pre-check failed, falling back to per-consumer locking every tick until it recovers: ${msg}`;
|
|
257
|
-
|
|
258
|
-
context.log.error(logMsg);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
console.error(logMsg);
|
|
262
|
-
}
|
|
266
|
+
logDispatcherError(logMsg);
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
269
|
// Every consumer runs its own turn concurrently (own TX, own cursor,
|
|
@@ -396,15 +400,7 @@ export function createEventDispatcher(options) {
|
|
|
396
400
|
// callers and at-least-once-with-duplicate-delivery on the next pass;
|
|
397
401
|
// neither is what we want, so ops needs to see it.
|
|
398
402
|
const msg = e instanceof Error ? e.message : String(e);
|
|
399
|
-
|
|
400
|
-
context.log.error(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
// No logger wired onto this context (e.g. createEventDispatcher used
|
|
404
|
-
// standalone) — console.error is the only way ops still sees this in
|
|
405
|
-
// pod logs instead of the failure vanishing with the rolled-back tx.
|
|
406
|
-
console.error(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
407
|
-
}
|
|
403
|
+
logDispatcherError(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
408
404
|
span.setStatus("error", msg);
|
|
409
405
|
// The whole pass rolled back, so markProcessing/persistConsumerOutcome
|
|
410
406
|
// never committed — attempts/last_error are stuck at their pre-pass
|
|
@@ -413,18 +409,13 @@ export function createEventDispatcher(options) {
|
|
|
413
409
|
// so any failure here is swallowed after logging.
|
|
414
410
|
try {
|
|
415
411
|
await db.begin(async (tx) => {
|
|
416
|
-
await
|
|
412
|
+
await recordConsumerPassFailure(tx, consumer.name, instanceId, msg);
|
|
417
413
|
});
|
|
418
414
|
}
|
|
419
415
|
catch (persistErr) {
|
|
420
416
|
const persistMsg = persistErr instanceof Error ? persistErr.message : String(persistErr);
|
|
421
417
|
const failureMsg = `[event-dispatcher] ${consumer.name} failed to persist pass-failure state: ${persistMsg}`;
|
|
422
|
-
|
|
423
|
-
context.log.error(failureMsg);
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
console.error(failureMsg);
|
|
427
|
-
}
|
|
418
|
+
logDispatcherError(failureMsg);
|
|
428
419
|
}
|
|
429
420
|
// Escalate the backoff so this consumer isn't retried on the very
|
|
430
421
|
// next tick — see PASS_FAILURE_BASE_BACKOFF_MS/MAX_MS above.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isAnonymousSessionUser } from "../engine/system-user.js";
|
|
2
|
+
import { AccessDeniedError, UnauthenticatedError } from "../errors/index.js";
|
|
3
|
+
// Anonymous callers get 401 so a client whose session lapsed (browser dropped
|
|
4
|
+
// the expired auth cookie) can tell "sign in again" apart from "signed in but
|
|
5
|
+
// not allowed", which stays 403.
|
|
6
|
+
export function handlerAccessError(user, type) {
|
|
7
|
+
if (isAnonymousSessionUser(user)) {
|
|
8
|
+
return new UnauthenticatedError({
|
|
9
|
+
message: `${type} requires a signed-in user`,
|
|
10
|
+
details: { handler: type },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return new AccessDeniedError({
|
|
14
|
+
message: `access denied for ${type}`,
|
|
15
|
+
details: { handler: type },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -28,11 +28,17 @@ function assertEntryHandlerHighRisk(args) {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
// Stream handlers have no agent hint, so the "declare risk high" advice cannot apply to them.
|
|
32
|
+
function highRiskRemedy(entryHandler) {
|
|
33
|
+
return entryHandler.qn.includes(":stream:")
|
|
34
|
+
? 'stream handlers cannot declare an agent risk and always resolve "mid" — dispatch a risk "high" write handler directly instead'
|
|
35
|
+
: `declare agent: { risk: "high" } on "${entryHandler.qn}"`;
|
|
36
|
+
}
|
|
31
37
|
export function assertIrreversibleOperationAllowed(operation) {
|
|
32
38
|
assertEntryHandlerHighRisk({
|
|
33
39
|
reason: FrameworkReasons.irreversibleOperationRequiresHighRisk,
|
|
34
40
|
buildMessage: (entryHandler) => `Handler "${entryHandler.qn}" performs an irreversible ${operation} but resolves agent risk ` +
|
|
35
|
-
`"${entryHandler.risk}" —
|
|
41
|
+
`"${entryHandler.risk}" — ${highRiskRemedy(entryHandler)}. The ` +
|
|
36
42
|
"directly called handler must carry it; delegating via ctx.write/writeAs does not.",
|
|
37
43
|
details: { operation },
|
|
38
44
|
});
|
|
@@ -49,8 +55,8 @@ export function assertInstructionFieldWriteAllowed(entityName, verb, writtenFiel
|
|
|
49
55
|
assertEntryHandlerHighRisk({
|
|
50
56
|
reason: FrameworkReasons.instructionFieldWriteRequiresHighRisk,
|
|
51
57
|
buildMessage: (entryHandler) => `Handler "${entryHandler.qn}" ${verb}s "${entityName}" field(s) ${fields.join(", ")} ` +
|
|
52
|
-
`flagged readAsInstruction: true but resolves agent risk "${entryHandler.risk}" —
|
|
53
|
-
|
|
58
|
+
`flagged readAsInstruction: true but resolves agent risk "${entryHandler.risk}" — ` +
|
|
59
|
+
`${highRiskRemedy(entryHandler)}. The directly called handler must carry ` +
|
|
54
60
|
"it; delegating via ctx.write/writeAs does not.",
|
|
55
61
|
details: { entityName, verb, fields },
|
|
56
62
|
});
|
|
@@ -6,8 +6,13 @@ const PG_READ_ONLY_SQLSTATE = "25006";
|
|
|
6
6
|
function isReadOnlyTransactionViolation(e) {
|
|
7
7
|
return extractPgError(e)?.code === PG_READ_ONLY_SQLSTATE;
|
|
8
8
|
}
|
|
9
|
+
class SavepointDiscard extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("savepoint discarded on purpose");
|
|
12
|
+
this.name = "SavepointDiscard";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
9
15
|
async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
10
|
-
const discardMarker = {};
|
|
11
16
|
const settled = {};
|
|
12
17
|
try {
|
|
13
18
|
await runInSavepoint(tx, async (sp) => {
|
|
@@ -16,11 +21,11 @@ async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
|
16
21
|
// @cast-boundary driver savepoint handle — structurally a DbTx, same shape runInSavepoint's caller relies on
|
|
17
22
|
settled.outcome = { value: await fn(sp) };
|
|
18
23
|
// Always discarded: RELEASE SAVEPOINT would leave the outer tx read-only too.
|
|
19
|
-
throw
|
|
24
|
+
throw new SavepointDiscard();
|
|
20
25
|
});
|
|
21
26
|
}
|
|
22
27
|
catch (e) {
|
|
23
|
-
if (e
|
|
28
|
+
if (!(e instanceof SavepointDiscard))
|
|
24
29
|
throw e;
|
|
25
30
|
}
|
|
26
31
|
if (!settled.outcome) {
|
|
@@ -204,7 +204,13 @@ async function readProjectionRowForSearch(ctx, registry, entityName, entity, ten
|
|
|
204
204
|
const row = await createTenantDb(baseDb, tenantId).fetchOne(table, where);
|
|
205
205
|
if (!row)
|
|
206
206
|
return undefined;
|
|
207
|
-
|
|
207
|
+
const state = rehydrateCompoundTypes(row, entity);
|
|
208
|
+
// The event payload never carries sensitive fields; extensions must not see them via the row either.
|
|
209
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
210
|
+
if ("sensitive" in field && field.sensitive === true)
|
|
211
|
+
delete state[name];
|
|
212
|
+
}
|
|
213
|
+
return state;
|
|
208
214
|
}
|
|
209
215
|
// #1610 — subject-annotated searchable fields are ciphertext in the event
|
|
210
216
|
// payload; decrypt into the derived index only. No KMS → omit ciphertext
|
|
@@ -13,7 +13,7 @@ function hasSameClaims(caller, asUser) {
|
|
|
13
13
|
if (keys.length !== Object.keys(asUserClaims).length)
|
|
14
14
|
return false;
|
|
15
15
|
return keys.every((key) => Object.hasOwn(asUserClaims, key) &&
|
|
16
|
-
|
|
16
|
+
Bun.deepEquals(callerClaims[key], asUserClaims[key], true));
|
|
17
17
|
}
|
|
18
18
|
// claims drive ownership row filters and origin makes a ctx read-only, so both must match exactly.
|
|
19
19
|
export function isSelfDelegation(caller, asUser) {
|
|
@@ -153,7 +153,6 @@ export function createMeilisearchAdapter(options) {
|
|
|
153
153
|
// skip: empty batch — avoid an unnecessary Meilisearch round-trip
|
|
154
154
|
if (items.length === 0)
|
|
155
155
|
return;
|
|
156
|
-
await ensureConfigured(tenantId);
|
|
157
156
|
const index = client.index(meilisearchTenantIndex(prefix, tenantId));
|
|
158
157
|
const ids = items.map((i) => meilisearchDocId(i.entityType, i.entityId));
|
|
159
158
|
await awaitSucceededTask(index.deleteDocuments(ids), REMOVE_TOLERATED_ERROR_CODES);
|
|
@@ -178,7 +177,6 @@ export function createMeilisearchAdapter(options) {
|
|
|
178
177
|
}));
|
|
179
178
|
},
|
|
180
179
|
async remove(tenantId, entityType, entityId) {
|
|
181
|
-
await ensureConfigured(tenantId);
|
|
182
180
|
const index = client.index(meilisearchTenantIndex(prefix, tenantId));
|
|
183
181
|
await awaitSucceededTask(index.deleteDocument(meilisearchDocId(entityType, entityId)), REMOVE_TOLERATED_ERROR_CODES);
|
|
184
182
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KUMIKO_NAME_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
2
|
import { columnNamesOf, tableExists } from "../db/schema-inspection.js";
|
|
3
|
-
import { addMissingColumns, tableToMeta, unsafePushTables } from "./table-helpers.js";
|
|
3
|
+
import { addMissingColumns, addMissingIndexes, tableToMeta, unsafePushTables, } from "./table-helpers.js";
|
|
4
4
|
// biome-ignore lint/suspicious/noConsole: stack-internal status logging
|
|
5
5
|
const logInfo = (msg) => console.log(msg);
|
|
6
6
|
/**
|
|
@@ -44,7 +44,7 @@ export async function pushEntityProjectionTables(stack, registry) {
|
|
|
44
44
|
function isUnsafeToAutoAdd(col) {
|
|
45
45
|
return col.notNull && !col.primaryKey && col.defaultSql === undefined;
|
|
46
46
|
}
|
|
47
|
-
// Backfills columns a persistent dev DB predates; a required column with no
|
|
47
|
+
// Backfills columns and indexes a persistent dev DB predates; a required column with no
|
|
48
48
|
// default can't be added safely on a table that may hold rows, so that
|
|
49
49
|
// stays a boot error instead.
|
|
50
50
|
async function syncExistingProjectionTable(stack, physical, table) {
|
|
@@ -53,7 +53,8 @@ async function syncExistingProjectionTable(stack, physical, table) {
|
|
|
53
53
|
const missingColumns = meta.columns.filter((c) => !liveColumns.has(c.name));
|
|
54
54
|
if (missingColumns.length === 0) {
|
|
55
55
|
logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
|
|
56
|
-
|
|
56
|
+
await addMissingIndexes(stack.db, physical, meta.indexes);
|
|
57
|
+
// skip: table is already in sync, only indexes needed backfilling
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
const unsafeColumns = missingColumns.filter(isUnsafeToAutoAdd);
|
|
@@ -70,4 +71,5 @@ async function syncExistingProjectionTable(stack, physical, table) {
|
|
|
70
71
|
logInfo(`[kumiko-stack] table ${physical} is missing column(s) ` +
|
|
71
72
|
`${missingColumns.map((c) => `"${c.name}"`).join(", ")} — adding`);
|
|
72
73
|
await addMissingColumns(stack.db, physical, missingColumns);
|
|
74
|
+
await addMissingIndexes(stack.db, physical, meta.indexes);
|
|
73
75
|
}
|
package/dist/stack/redis.d.ts
CHANGED
package/dist/stack/redis.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { generateId } from "../utils/index.js";
|
|
2
2
|
import { requireEnv } from "./db.js";
|
|
3
|
+
// BullMQ queues live on the raw redisUrl, outside the test Redis keyPrefix, so
|
|
4
|
+
// parallel stacks sharing the prod default queue name would consume each
|
|
5
|
+
// other's jobs. BullMQ rejects `:` in queue names.
|
|
6
|
+
export function queueNamePrefixForTestRedis(keyPrefix) {
|
|
7
|
+
return keyPrefix.split(":").filter(Boolean).join("-");
|
|
8
|
+
}
|
|
3
9
|
export async function createTestRedis() {
|
|
4
10
|
const Redis = (await import("ioredis")).Redis;
|
|
5
11
|
const redisUrl = requireEnv("REDIS_URL");
|
|
@@ -8,13 +14,13 @@ export async function createTestRedis() {
|
|
|
8
14
|
// prefix space is unbounded. See Track B.3 in docs/plans/tests-refactor.
|
|
9
15
|
const prefix = `kt:${generateId().slice(-8)}:`;
|
|
10
16
|
const redis = new Redis(redisUrl, { keyPrefix: prefix });
|
|
11
|
-
async function
|
|
17
|
+
async function deleteKeysMatching(pattern) {
|
|
12
18
|
// Open a prefix-less client for the scan — ioredis' keyPrefix is applied
|
|
13
19
|
// per-command but SCAN's returned keys are full names, so managing the
|
|
14
20
|
// del set with the prefix already on the connection is error-prone.
|
|
15
21
|
const raw = new Redis(redisUrl);
|
|
16
22
|
try {
|
|
17
|
-
const stream = raw.scanStream({ match:
|
|
23
|
+
const stream = raw.scanStream({ match: pattern, count: 500 });
|
|
18
24
|
const keys = [];
|
|
19
25
|
for await (const batch of stream)
|
|
20
26
|
keys.push(...batch);
|
|
@@ -25,6 +31,7 @@ export async function createTestRedis() {
|
|
|
25
31
|
raw.disconnect();
|
|
26
32
|
}
|
|
27
33
|
}
|
|
34
|
+
const flushNamespace = () => deleteKeysMatching(`${prefix}*`);
|
|
28
35
|
return {
|
|
29
36
|
redis,
|
|
30
37
|
redisUrl,
|
|
@@ -32,6 +39,8 @@ export async function createTestRedis() {
|
|
|
32
39
|
flushNamespace,
|
|
33
40
|
cleanup: async () => {
|
|
34
41
|
await flushNamespace();
|
|
42
|
+
// The derived per-stack JobRunner queues sit outside the keyPrefix.
|
|
43
|
+
await deleteKeysMatching(`bull:${queueNamePrefixForTestRedis(prefix)}-*`);
|
|
35
44
|
redis.disconnect();
|
|
36
45
|
},
|
|
37
46
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbConnection } from "../db/connection.js";
|
|
2
|
-
import type { ColumnMeta, EntityTableMeta } from "../db/entity-table-meta.js";
|
|
2
|
+
import type { ColumnMeta, EntityTableMeta, IndexMeta } from "../db/entity-table-meta.js";
|
|
3
3
|
import type { EventDispatcher } from "../pipeline/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* Bypass: creates an entity-table directly without going through the
|
|
@@ -11,6 +11,8 @@ export declare function unsafeEnsureEntityTable(db: DbConnection, entity: import
|
|
|
11
11
|
export declare function tableToMeta(table: unknown): EntityTableMeta;
|
|
12
12
|
/** Applies ALTER TABLE ADD COLUMN for exactly the given columns. */
|
|
13
13
|
export declare function addMissingColumns(db: DbConnection, tableName: string, columns: readonly ColumnMeta[]): Promise<void>;
|
|
14
|
+
/** CREATE [UNIQUE] INDEX IF NOT EXISTS for exactly the given indexes. */
|
|
15
|
+
export declare function addMissingIndexes(db: DbConnection, tableName: string, indexes: readonly IndexMeta[]): Promise<void>;
|
|
14
16
|
/**
|
|
15
17
|
* Bypass: pushes table definitions to the database directly. Produces
|
|
16
18
|
* CREATE TABLE IF NOT EXISTS + CREATE INDEX statements via renderTableDdl
|
|
@@ -66,6 +66,14 @@ export async function addMissingColumns(db, tableName, columns) {
|
|
|
66
66
|
await alterTableAddColumn(db, tableName, col.name, type, defaultClause, notNull);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/** CREATE [UNIQUE] INDEX IF NOT EXISTS for exactly the given indexes. */
|
|
70
|
+
export async function addMissingIndexes(db, tableName, indexes) {
|
|
71
|
+
for (const idx of indexes) {
|
|
72
|
+
const kind = idx.unique ? "UNIQUE INDEX" : "INDEX";
|
|
73
|
+
const colList = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
74
|
+
await createIndexIfNotExists(db, kind, idx.name, tableName, colList, idx.whereSql);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
69
77
|
/**
|
|
70
78
|
* Bypass: pushes table definitions to the database directly. Produces
|
|
71
79
|
* CREATE TABLE IF NOT EXISTS + CREATE INDEX statements via renderTableDdl
|
|
@@ -90,13 +98,7 @@ export async function unsafePushTables(db, tables, prevTables) {
|
|
|
90
98
|
const newCols = meta.columns.filter((c) => !prevCols.has(c.name));
|
|
91
99
|
await addMissingColumns(db, meta.tableName, newCols);
|
|
92
100
|
const prevIdxNames = new Set(prev.indexes.map((i) => i.name));
|
|
93
|
-
|
|
94
|
-
if (!prevIdxNames.has(idx.name)) {
|
|
95
|
-
const kind = idx.unique ? "UNIQUE INDEX" : "INDEX";
|
|
96
|
-
const colList = idx.columns.map((c) => `"${c}"`).join(", ");
|
|
97
|
-
await createIndexIfNotExists(db, kind, idx.name, meta.tableName, colList, idx.whereSql);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
101
|
+
await addMissingIndexes(db, meta.tableName, meta.indexes.filter((i) => !prevIdxNames.has(i.name)));
|
|
100
102
|
}
|
|
101
103
|
else {
|
|
102
104
|
const statements = renderTableDdl(meta);
|
package/dist/stack/test-stack.js
CHANGED
|
@@ -11,16 +11,10 @@ import { createInMemorySearchAdapter } from "../search/index.js";
|
|
|
11
11
|
import { createTestDb } from "./db.js";
|
|
12
12
|
import { createJobFailureTracker, drainJobs } from "./drain-jobs.js";
|
|
13
13
|
import { createEventCollector } from "./event-collector.js";
|
|
14
|
-
import { createTestRedis } from "./redis.js";
|
|
14
|
+
import { createTestRedis, queueNamePrefixForTestRedis } from "./redis.js";
|
|
15
15
|
import { createRequestHelper } from "./request-helper.js";
|
|
16
16
|
import { unsafePushTables } from "./table-helpers.js";
|
|
17
17
|
const DEFAULT_JWT_SECRET = "test-stack-secret-minimum-32-characters!!";
|
|
18
|
-
// BullMQ queues live on the raw redisUrl, outside the test Redis keyPrefix, so
|
|
19
|
-
// parallel stacks sharing the prod default queue name would consume each
|
|
20
|
-
// other's jobs. BullMQ rejects `:` in queue names.
|
|
21
|
-
function queueNamePrefixForTestRedis(keyPrefix) {
|
|
22
|
-
return keyPrefix.split(":").filter(Boolean).join("-");
|
|
23
|
-
}
|
|
24
18
|
export async function setupTestStack(options) {
|
|
25
19
|
const jwtSecret = options.jwtSecret ?? DEFAULT_JWT_SECRET;
|
|
26
20
|
const enabledHooks = options.systemHooks ?? ["sse", "search"];
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// production services (delivery-service uses it to run cross-feature notify
|
|
6
6
|
// calls without a real dispatcher). Hence the runtime classification despite
|
|
7
7
|
// living under `testing/` — no vitest imports, no test side-effects.
|
|
8
|
-
import {
|
|
8
|
+
import { createAnonymousUser } from "../engine/system-user.js";
|
|
9
9
|
import { DEFAULT_LOCALE } from "../i18n/request-locale.js";
|
|
10
10
|
import { createNoopMetricsHandle, getFallbackTracer } from "../observability/index.js";
|
|
11
11
|
import { createTzContext } from "../time/index.js";
|
|
@@ -31,11 +31,8 @@ export function bridgeStub(opts) {
|
|
|
31
31
|
// anonymous default values when no user is passed explicitly. Test code
|
|
32
32
|
// that cares about identity passes its own SessionUser here and gets it
|
|
33
33
|
// back on ctx.
|
|
34
|
-
const stubUser = opts?.user ??
|
|
35
|
-
|
|
36
|
-
tenantId: "00000000-0000-0000-0000-000000000000", // @cast-boundary engine-bridge
|
|
37
|
-
roles: [ANONYMOUS_ROLE],
|
|
38
|
-
};
|
|
34
|
+
const stubUser = opts?.user ??
|
|
35
|
+
createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
|
|
39
36
|
return {
|
|
40
37
|
user: stubUser,
|
|
41
38
|
query: notAvailable("query"), // @cast-boundary engine-bridge
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPe
|
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
11
11
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
|
|
12
12
|
export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, 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";
|
|
13
|
-
export { metricField } from "../engine/types/screen.js";
|
|
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";
|
|
16
16
|
export type { WorkspaceDefinition } from "../engine/types/workspace.js";
|
package/dist/ui-types/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { computeDerivedCellValue, roundDerivedCellValue } from "../engine/embedd
|
|
|
24
24
|
export { parseRefTarget } from "../engine/parse-ref-target.js";
|
|
25
25
|
export { evalFieldCondition, explicitListScreenId, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
|
|
26
26
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
27
|
-
export { metricField } from "../engine/types/screen.js";
|
|
27
|
+
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
|
|
28
28
|
export { ACTION_FORM_ENTITY, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
|
|
29
29
|
export { formalLocaleTag } from "./formality.js";
|
|
30
30
|
export { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "./list-row-meta.js";
|