@cosmicdrift/kumiko-framework 0.350.0 → 0.352.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/bun-db/query.js +3 -3
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +4 -3
- package/dist/db/located-timestamp.js +1 -1
- package/dist/db/queries/seed-context.d.ts +17 -0
- package/dist/db/queries/seed-context.js +26 -0
- package/dist/db/table-builder.d.ts +5 -5
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/query-output-columns.js +3 -3
- package/dist/engine/boot-validator/query-refs.js +11 -7
- package/dist/engine/boot-validator/screens.js +89 -9
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/engine/steps/_duration-utils.js +1 -1
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/es-ops/context.js +15 -1
- package/dist/es-ops/index.d.ts +1 -1
- package/dist/es-ops/types.d.ts +20 -0
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +4 -17
- package/dist/event-store/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- package/dist/event-store/tenant-db-append.d.ts +8 -0
- package/dist/event-store/tenant-db-append.js +16 -0
- package/dist/i18n/required-surface-keys.js +7 -1
- package/dist/pipeline/dispatch-batch.js +5 -1
- package/dist/pipeline/dispatch-write.js +36 -82
- package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -0
- package/dist/pipeline/event-dispatcher-delivery.js +3 -10
- package/dist/pipeline/pre-handler-gates.d.ts +9 -0
- package/dist/pipeline/pre-handler-gates.js +104 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +12 -3
- package/dist/pipeline/pre-transaction-reservations.js +34 -21
- package/dist/rate-limit/resolver.js +3 -6
- package/dist/time/legacy-date.d.ts +1 -1
- package/dist/time/legacy-date.js +1 -1
- package/dist/time/polyfill.d.ts +1 -0
- package/dist/time/polyfill.js +14 -33
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +8 -11
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/upgrade-cli.js +39 -35
- package/package.json +4 -4
- package/src/changes.json +27 -0
package/dist/bun-db/query.js
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// drizzle's getTableName + getTableColumns (drizzle kept only as a type
|
|
20
20
|
// reference, never as a runtime API call)
|
|
21
21
|
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
22
|
-
// Static
|
|
23
|
-
// Temporal
|
|
24
|
-
import { Temporal } from "temporal
|
|
22
|
+
// Static import (not getTemporal()): coercion works without an installed
|
|
23
|
+
// global and returns the same Temporal classes as the rest of the repo (#1480).
|
|
24
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
25
25
|
import { requestContext } from "../api/request-context.js";
|
|
26
26
|
import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
|
|
27
27
|
import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
|
package/dist/db/dialect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ColumnHandle, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
|
-
import { Temporal } from "temporal
|
|
2
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
import type { PgType } from "./entity-table-meta.js";
|
|
4
4
|
export type { ColumnHandle, SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
5
5
|
export type TableColumns<_T = any> = SchemaTable;
|
package/dist/db/dialect.js
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// The framework no longer imports drizzle-orm at runtime — schema-files
|
|
14
14
|
// use only this module.
|
|
15
15
|
//
|
|
16
|
-
// Static import, not the ambient global:
|
|
17
|
-
//
|
|
16
|
+
// Static import of the shared Temporal module, not the ambient global: the
|
|
17
|
+
// global may be absent (Hermes/Safari) or torn down in tests, and instantToDriver
|
|
18
|
+
// crashed on timestamptz writes without it (#1480).
|
|
18
19
|
import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
19
|
-
import { Temporal } from "temporal
|
|
20
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
20
21
|
function isNumericPgType(t) {
|
|
21
22
|
return t.startsWith("numeric(");
|
|
22
23
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Static import, not the ambient global: Bun doesn't expose Temporal on
|
|
12
12
|
// globalThis, so this crashed with "Temporal is not defined" outside boot
|
|
13
13
|
// paths that install it (#1480).
|
|
14
|
-
import { Temporal } from "temporal
|
|
14
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
15
15
|
// Sprint F: <name>Utc-Spalte ist jetzt instant() (siehe dialect.ts) —
|
|
16
16
|
// Drizzle gibt direkt Temporal.Instant zurück. Vor Sprint F kam ein PG-
|
|
17
17
|
// Wire-Format-String "2026-04-15 09:00:00+00" rein der via String-Massage
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DbRunner } from "../connection.js";
|
|
1
2
|
import type { AnyDb } from "../query.js";
|
|
2
3
|
export type SeedUserRow = {
|
|
3
4
|
readonly id: string;
|
|
@@ -18,3 +19,19 @@ export type SeedTenantDbRow = {
|
|
|
18
19
|
export declare function selectUserByEmail(db: AnyDb, email: string): Promise<SeedUserRow | null>;
|
|
19
20
|
export declare function selectMembershipsOfUser(db: AnyDb, userId: string): Promise<readonly SeedMembershipDbRow[]>;
|
|
20
21
|
export declare function selectAllTenants(db: AnyDb): Promise<readonly SeedTenantDbRow[]>;
|
|
22
|
+
export type SeedTemplateResourceDbRow = {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly tenant_id: string;
|
|
25
|
+
readonly slug: string;
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly locale: string;
|
|
28
|
+
readonly status: string;
|
|
29
|
+
};
|
|
30
|
+
export type SeedTemplateResourceDbFilter = {
|
|
31
|
+
readonly tenantId: string;
|
|
32
|
+
readonly slug?: string;
|
|
33
|
+
readonly kind?: string;
|
|
34
|
+
readonly status?: string;
|
|
35
|
+
readonly locale?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function selectTemplateResources(db: DbRunner, filter: SeedTemplateResourceDbFilter): Promise<readonly SeedTemplateResourceDbRow[]>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { unsafeReadRetrying } from "../query.js";
|
|
2
|
+
import { tableExists } from "../schema-inspection.js";
|
|
3
|
+
import { SOFT_DELETE_LIVE_ROW_PREDICATE } from "../table-builder.js";
|
|
2
4
|
export async function selectUserByEmail(db, email) {
|
|
3
5
|
const rows = await unsafeReadRetrying(db, `SELECT id::text AS id, email, tenant_id::text AS tenant_id
|
|
4
6
|
FROM read_users
|
|
@@ -23,3 +25,27 @@ export async function selectAllTenants(db) {
|
|
|
23
25
|
FROM read_tenants
|
|
24
26
|
ORDER BY inserted_at`, []);
|
|
25
27
|
}
|
|
28
|
+
// Column names are fixed fragments; every filter value is a bound parameter.
|
|
29
|
+
// template-resolver is an optional feature — without its table the answer is
|
|
30
|
+
// "no templates", not an error.
|
|
31
|
+
export async function selectTemplateResources(db, filter) {
|
|
32
|
+
if (!(await tableExists(db, "read_template_resources")))
|
|
33
|
+
return [];
|
|
34
|
+
const conditions = ["tenant_id = $1", SOFT_DELETE_LIVE_ROW_PREDICATE];
|
|
35
|
+
const params = [filter.tenantId];
|
|
36
|
+
for (const [column, value] of [
|
|
37
|
+
["slug", filter.slug],
|
|
38
|
+
["kind", filter.kind],
|
|
39
|
+
["status", filter.status],
|
|
40
|
+
["locale", filter.locale],
|
|
41
|
+
]) {
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
params.push(value);
|
|
45
|
+
conditions.push(`${column} = $${params.length}`);
|
|
46
|
+
}
|
|
47
|
+
return unsafeReadRetrying(db, `SELECT id::text AS id, tenant_id::text AS tenant_id, slug, kind, locale, status
|
|
48
|
+
FROM read_template_resources
|
|
49
|
+
WHERE ${conditions.join(" AND ")}
|
|
50
|
+
ORDER BY slug, locale`, params);
|
|
51
|
+
}
|
|
@@ -167,20 +167,20 @@ export declare function buildBaseColumns(softDelete: boolean, idType?: "serial"
|
|
|
167
167
|
id: ColumnBuilder<string> | ColumnBuilder<number>;
|
|
168
168
|
tenantId: ColumnBuilder<string>;
|
|
169
169
|
version: ColumnBuilder<number>;
|
|
170
|
-
insertedAt: ColumnBuilder<
|
|
171
|
-
modifiedAt: ColumnBuilder<
|
|
170
|
+
insertedAt: ColumnBuilder<Temporal.Instant>;
|
|
171
|
+
modifiedAt: ColumnBuilder<Temporal.Instant>;
|
|
172
172
|
insertedById: ColumnBuilder<string>;
|
|
173
173
|
modifiedById: ColumnBuilder<string>;
|
|
174
174
|
} | {
|
|
175
175
|
id: ColumnBuilder<string> | ColumnBuilder<number>;
|
|
176
176
|
tenantId: ColumnBuilder<string>;
|
|
177
177
|
version: ColumnBuilder<number>;
|
|
178
|
-
insertedAt: ColumnBuilder<
|
|
179
|
-
modifiedAt: ColumnBuilder<
|
|
178
|
+
insertedAt: ColumnBuilder<Temporal.Instant>;
|
|
179
|
+
modifiedAt: ColumnBuilder<Temporal.Instant>;
|
|
180
180
|
insertedById: ColumnBuilder<string>;
|
|
181
181
|
modifiedById: ColumnBuilder<string>;
|
|
182
182
|
isDeleted: ColumnBuilder<boolean>;
|
|
183
|
-
deletedAt: ColumnBuilder<
|
|
183
|
+
deletedAt: ColumnBuilder<Temporal.Instant>;
|
|
184
184
|
deletedById: ColumnBuilder<string>;
|
|
185
185
|
};
|
|
186
186
|
export declare function rowMetaFieldNames(softDelete: boolean): ReadonlySet<string>;
|
|
@@ -19,6 +19,7 @@ export declare function validateFileFields(feature: FeatureDefinition): boolean;
|
|
|
19
19
|
export declare function validateReferenceFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
20
20
|
export declare function validateEmbeddedFields(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
21
21
|
export declare function validateEntitySelectOptionsQuery(feature: FeatureDefinition): void;
|
|
22
|
+
export declare function validateEntitySelectAvailabilityQuery(feature: FeatureDefinition, queryHandlers: ReadonlyMap<string, QueryHandlerDef>): void;
|
|
22
23
|
export declare function validateMultiSelectFields(feature: FeatureDefinition): void;
|
|
23
24
|
export declare function validateImageVariants(feature: FeatureDefinition): void;
|
|
24
25
|
export declare function validateTransitions(feature: FeatureDefinition): void;
|
|
@@ -692,6 +692,23 @@ function assertNoSelectOptionsQuery(field, path, featureName) {
|
|
|
692
692
|
throw new Error(`[Feature ${featureName}] Entity select field "${path}" declares optionsQuery — not supported on entity fields because the stored value would not be validated against the query result. Use static options, or a reference field.`);
|
|
693
693
|
}
|
|
694
694
|
}
|
|
695
|
+
// A typo'd availability QN would only surface as a failed query when the form
|
|
696
|
+
// mounts, so it is pinned at boot like the other option queries.
|
|
697
|
+
export function validateEntitySelectAvailabilityQuery(feature, queryHandlers) {
|
|
698
|
+
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
699
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
700
|
+
if (field.type !== "select" || field.optionsAvailabilityQuery === undefined)
|
|
701
|
+
continue;
|
|
702
|
+
const query = field.optionsAvailabilityQuery;
|
|
703
|
+
if (query.length === 0 || !queryHandlers.has(query)) {
|
|
704
|
+
throw new Error(`[Feature ${feature.name}] Select field "${fieldName}" on entity "${entityName}" ` +
|
|
705
|
+
`declares optionsAvailabilityQuery "${query}" which is not a registered query-handler. ` +
|
|
706
|
+
`Check the QN spelling (expected "<feature>:query:<short>") and that the handler ` +
|
|
707
|
+
`is declared via r.queryHandler(...).`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
695
712
|
export function validateMultiSelectFields(feature) {
|
|
696
713
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
697
714
|
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
@@ -9,7 +9,7 @@ import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.
|
|
|
9
9
|
import { validateFeatureBootChecks } from "./boot-check.js";
|
|
10
10
|
import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, validateConfigReads, warnOnToggleableDependencies, } from "./config-deps.js";
|
|
11
11
|
import { collectDetailForScreens } from "./detail-screens.js";
|
|
12
|
-
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
|
|
12
|
+
import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectAvailabilityQuery, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
|
|
13
13
|
import { validateEntityListScreens } from "./entity-list-screens.js";
|
|
14
14
|
import { validateGdprStoragePersistence } from "./gdpr-storage.js";
|
|
15
15
|
import { validateGlobalTenancyEntities } from "./global-tenancy.js";
|
|
@@ -169,6 +169,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
169
169
|
validateRecordOwnedSubjects(feature);
|
|
170
170
|
validateApiExposureMatching(feature, allExposedApis, featureMap);
|
|
171
171
|
validateEntitySelectOptionsQuery(feature);
|
|
172
|
+
validateEntitySelectAvailabilityQuery(feature, queryHandlerQns);
|
|
172
173
|
validateEmbeddedFields(feature, featureMap, queryHandlerQns);
|
|
173
174
|
validateMultiSelectFields(feature);
|
|
174
175
|
validateImageVariants(feature);
|
|
@@ -175,6 +175,9 @@ function checkVisibleWhenField(queryHandlers, visibleWhen, owner) {
|
|
|
175
175
|
}
|
|
176
176
|
function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen) {
|
|
177
177
|
for (const panel of screen.panels) {
|
|
178
|
+
if (panel.kind !== "custom") {
|
|
179
|
+
checkVisibleWhenField(queryHandlers, panel.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}"`);
|
|
180
|
+
}
|
|
178
181
|
if (panel.kind === "stat") {
|
|
179
182
|
checkDashboardStatPanelFields(queryHandlers, featureName, screenId, panel);
|
|
180
183
|
}
|
|
@@ -183,9 +186,6 @@ function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen
|
|
|
183
186
|
checkDashboardStatPanelFields(queryHandlers, featureName, screenId, stat);
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
|
-
else if (panel.kind === "screen") {
|
|
187
|
-
checkVisibleWhenField(queryHandlers, panel.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}"`);
|
|
188
|
-
}
|
|
189
189
|
else if (panel.kind === "list") {
|
|
190
190
|
const rowShape = resolveListRowShape(queryHandlers, panel.query, `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`);
|
|
191
191
|
for (const column of panel.columns) {
|
|
@@ -26,12 +26,11 @@ function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
26
26
|
for (const panel of screen.panels) {
|
|
27
27
|
if (panel.kind === "custom")
|
|
28
28
|
continue;
|
|
29
|
-
if (panel.
|
|
30
|
-
|
|
31
|
-
checkQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}" visibleWhen`);
|
|
32
|
-
}
|
|
33
|
-
continue;
|
|
29
|
+
if (panel.visibleWhen !== undefined) {
|
|
30
|
+
checkQueryRef(queryHandlers, panel.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) ${panel.kind === "screen" ? "screen-panel" : "panel"} "${panel.id}" visibleWhen`);
|
|
34
31
|
}
|
|
32
|
+
if (panel.kind === "screen")
|
|
33
|
+
continue;
|
|
35
34
|
if (panel.kind === "stat-group") {
|
|
36
35
|
for (const stat of panel.stats) {
|
|
37
36
|
checkQueryRef(queryHandlers, stat.query, () => `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}"`);
|
|
@@ -46,9 +45,14 @@ function checkDashboardQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
|
46
45
|
}
|
|
47
46
|
function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenType, fields) {
|
|
48
47
|
for (const [fieldName, field] of Object.entries(fields)) {
|
|
49
|
-
if (field.type !== "select"
|
|
48
|
+
if (field.type !== "select")
|
|
50
49
|
continue;
|
|
51
|
-
|
|
50
|
+
if (field.optionsQuery !== undefined) {
|
|
51
|
+
checkQueryRef(queryHandlers, field.optionsQuery, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) select field "${fieldName}" optionsQuery`);
|
|
52
|
+
}
|
|
53
|
+
if (field.optionsAvailabilityQuery !== undefined) {
|
|
54
|
+
checkQueryRef(queryHandlers, field.optionsAvailabilityQuery, () => `[Feature ${featureName}] Screen "${screenId}" (${screenType}) select field "${fieldName}" optionsAvailabilityQuery`);
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
@@ -493,7 +493,8 @@ function rejectWriteOnlyFormField(featureName, screenId, context, fieldName, fde
|
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
// Static options XOR optionsQuery on inline-form select fields, and `{ field }`
|
|
496
|
-
// payload refs must name another field of the same form
|
|
496
|
+
// payload refs must name another field of the same form (the payload also feeds
|
|
497
|
+
// optionsAvailabilityQuery). The QN existence check
|
|
497
498
|
// lives in query-refs.ts with the other query refs.
|
|
498
499
|
function validateFormSelectOptions(featureName, screenId, context, fieldName, fdef, siblingFieldNames) {
|
|
499
500
|
if (fdef.type === "select") {
|
|
@@ -507,6 +508,12 @@ function validateFormSelectOptions(featureName, screenId, context, fieldName, fd
|
|
|
507
508
|
}
|
|
508
509
|
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
509
510
|
}
|
|
511
|
+
else if (fdef.optionsAvailabilityQuery !== undefined) {
|
|
512
|
+
if (fdef.optionsAvailabilityQuery.length === 0) {
|
|
513
|
+
throw new Error(`${where} has an empty optionsAvailabilityQuery`);
|
|
514
|
+
}
|
|
515
|
+
validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
|
|
516
|
+
}
|
|
510
517
|
else if (fdef.optionsQueryPayload !== undefined) {
|
|
511
518
|
throw new Error(`${where} has optionsQueryPayload without optionsQuery`);
|
|
512
519
|
}
|
|
@@ -1729,17 +1736,33 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
|
|
|
1729
1736
|
`navigate away from the dashboard on submit or cancel. Embed a screen without redirect and ` +
|
|
1730
1737
|
`with cancelTarget: false (or omitted).`);
|
|
1731
1738
|
}
|
|
1732
|
-
|
|
1739
|
+
validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
|
|
1740
|
+
}
|
|
1741
|
+
function validateDashboardPanelVisibleWhen(context, visibleWhen) {
|
|
1733
1742
|
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
1734
1743
|
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
1735
1744
|
}
|
|
1736
1745
|
}
|
|
1746
|
+
function validateDashboardPanelSpan(featureName, screenId, panel) {
|
|
1747
|
+
if (panel.span !== undefined && panel.span !== "half" && panel.span !== "full") {
|
|
1748
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has span "${String(panel.span)}" — expected "half" or "full".`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1737
1751
|
function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId) {
|
|
1752
|
+
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}"`;
|
|
1738
1753
|
if (panel.stats.length === 0) {
|
|
1739
|
-
throw new Error(
|
|
1754
|
+
throw new Error(`${context} has an empty stats list.`);
|
|
1740
1755
|
}
|
|
1756
|
+
if (panel.subtitle !== undefined && panel.label === undefined) {
|
|
1757
|
+
throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header.`);
|
|
1758
|
+
}
|
|
1759
|
+
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
1760
|
+
validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
|
|
1741
1761
|
for (const stat of panel.stats) {
|
|
1742
1762
|
addPanelId(stat.id, "stat-group child");
|
|
1763
|
+
if (stat.visibleWhen !== undefined) {
|
|
1764
|
+
throw new Error(`${context} child "${stat.id}" sets visibleWhen — gate the whole group instead; a hidden cell would leave a gap in the strip.`);
|
|
1765
|
+
}
|
|
1743
1766
|
if (!stat.query || typeof stat.query !== "string") {
|
|
1744
1767
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`);
|
|
1745
1768
|
}
|
|
@@ -1758,6 +1781,67 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
|
|
|
1758
1781
|
if (panel.scrollable === true && panel.chart !== "stacked-area") {
|
|
1759
1782
|
throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
1760
1783
|
}
|
|
1784
|
+
validateStackedAreaOnlyProps(where, panel);
|
|
1785
|
+
if (panel.legendTotals !== undefined && typeof panel.legendTotals !== "boolean") {
|
|
1786
|
+
throw new Error(`${where} has a non-boolean legendTotals.`);
|
|
1787
|
+
}
|
|
1788
|
+
for (const [key, color] of Object.entries(panel.seriesColors ?? {})) {
|
|
1789
|
+
if (typeof color !== "string" || color.trim() === "") {
|
|
1790
|
+
throw new Error(`${where} seriesColors["${key}"] must be a non-empty CSS color.`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
for (const [kind, look] of Object.entries(panel.markerKinds ?? {})) {
|
|
1794
|
+
validateDashboardChartMarkerKind(`${where} markerKinds["${kind}"]`, look);
|
|
1795
|
+
}
|
|
1796
|
+
if (panel.ranges !== undefined)
|
|
1797
|
+
validateDashboardChartRanges(`${where} ranges`, panel.ranges);
|
|
1798
|
+
}
|
|
1799
|
+
function validateStackedAreaOnlyProps(where, panel) {
|
|
1800
|
+
// skip: stacked-area is the one chart kind that supports these props
|
|
1801
|
+
if (panel.chart === "stacked-area")
|
|
1802
|
+
return;
|
|
1803
|
+
const stackedAreaOnly = ["markerKinds", "legendTotals", "ranges"].filter((prop) => panel[prop] !== undefined);
|
|
1804
|
+
if (stackedAreaOnly.length > 0) {
|
|
1805
|
+
throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it.`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
const DASHBOARD_CHART_TONES = new Set([
|
|
1809
|
+
"positive",
|
|
1810
|
+
"negative",
|
|
1811
|
+
"active",
|
|
1812
|
+
"neutral",
|
|
1813
|
+
]);
|
|
1814
|
+
function validateDashboardChartMarkerKind(where, look) {
|
|
1815
|
+
if (look.tone === undefined && look.color === undefined) {
|
|
1816
|
+
throw new Error(`${where} needs a tone or a color.`);
|
|
1817
|
+
}
|
|
1818
|
+
if (look.tone !== undefined && !DASHBOARD_CHART_TONES.has(look.tone)) {
|
|
1819
|
+
throw new Error(`${where}.tone "${String(look.tone)}" is not one of ${[...DASHBOARD_CHART_TONES].join(", ")}.`);
|
|
1820
|
+
}
|
|
1821
|
+
if (look.color !== undefined && (typeof look.color !== "string" || look.color.trim() === "")) {
|
|
1822
|
+
throw new Error(`${where}.color must be a non-empty CSS color.`);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
function validateDashboardChartRanges(where, ranges) {
|
|
1826
|
+
if (ranges.options.length === 0) {
|
|
1827
|
+
throw new Error(`${where}.options is empty — declare at least one option.`);
|
|
1828
|
+
}
|
|
1829
|
+
const values = new Set();
|
|
1830
|
+
for (const option of ranges.options) {
|
|
1831
|
+
if (option.value.length === 0 || option.label.length === 0) {
|
|
1832
|
+
throw new Error(`${where} option needs a non-empty value and label.`);
|
|
1833
|
+
}
|
|
1834
|
+
if (values.has(option.value)) {
|
|
1835
|
+
throw new Error(`${where}.options has duplicate value "${option.value}".`);
|
|
1836
|
+
}
|
|
1837
|
+
if (option.months !== undefined && (!Number.isInteger(option.months) || option.months <= 0)) {
|
|
1838
|
+
throw new Error(`${where} option "${option.value}" months "${String(option.months)}" must be a positive integer.`);
|
|
1839
|
+
}
|
|
1840
|
+
values.add(option.value);
|
|
1841
|
+
}
|
|
1842
|
+
if (!values.has(ranges.default)) {
|
|
1843
|
+
throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}).`);
|
|
1844
|
+
}
|
|
1761
1845
|
}
|
|
1762
1846
|
const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
|
|
1763
1847
|
const MAX_VALUE_FORMAT_FRACTION_DIGITS = 4;
|
|
@@ -1785,18 +1869,14 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
|
|
|
1785
1869
|
if (!panel.query || typeof panel.query !== "string") {
|
|
1786
1870
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`);
|
|
1787
1871
|
}
|
|
1788
|
-
|
|
1789
|
-
panel.span !== undefined &&
|
|
1790
|
-
panel.span !== "half" &&
|
|
1791
|
-
panel.span !== "full") {
|
|
1792
|
-
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has span "${String(panel.span)}" — expected "half" or "full".`);
|
|
1793
|
-
}
|
|
1872
|
+
validateDashboardPanelSpan(featureName, screenId, panel);
|
|
1794
1873
|
if (panel.kind === "stat") {
|
|
1795
1874
|
if (panel.valueField.length === 0) {
|
|
1796
1875
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
|
|
1797
1876
|
}
|
|
1798
1877
|
validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
|
|
1799
1878
|
}
|
|
1879
|
+
validateDashboardPanelVisibleWhen(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`, panel.visibleWhen);
|
|
1800
1880
|
if (panel.kind === "chart")
|
|
1801
1881
|
validateDashboardChartDisplay(featureName, screenId, panel);
|
|
1802
1882
|
if (panel.kind === "list") {
|
|
@@ -428,6 +428,9 @@ function projectField(fieldDef) {
|
|
|
428
428
|
// fw#2780: author-supplied picker source — without it the client falls back to the list handler.
|
|
429
429
|
if (typeof def["optionsQuery"] === "string")
|
|
430
430
|
out["optionsQuery"] = def["optionsQuery"];
|
|
431
|
+
// Select: tier/tenant availability of the static options, loaded by the renderer.
|
|
432
|
+
if (typeof def["optionsAvailabilityQuery"] === "string")
|
|
433
|
+
out["optionsAvailabilityQuery"] = def["optionsAvailabilityQuery"];
|
|
431
434
|
if (typeof def["multiple"] === "boolean")
|
|
432
435
|
out["multiple"] = def["multiple"];
|
|
433
436
|
// MultiSelect: display picks checkboxes vs. combobox in the renderer,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ISO-8601 duration arithmetic — shared by wait and waitForEvent steps.
|
|
2
2
|
// Accepts "P1D", "PT1H", "P1Y2M3DT4H5M6S" etc.
|
|
3
3
|
// Uses approximate calendar math (365d/year, 30d/month). See #23.
|
|
4
|
-
import { Temporal } from "temporal
|
|
4
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
export function addDuration(baseIso, duration) {
|
|
6
6
|
const pattern = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
|
7
7
|
const match = duration.match(pattern);
|
|
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
|
|
|
17
17
|
export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
|
|
18
18
|
export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionRowIdOf, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
|
|
19
19
|
export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
|
|
20
|
-
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
20
|
+
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelGate, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
21
21
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
22
22
|
export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
|
|
23
23
|
export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
|
package/dist/es-ops/context.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// SystemUser bypassed Access-Checks (Standard-Seed-Pattern, siehe
|
|
7
7
|
// config-seed.ts:40). Events haben createdBy = SYSTEM_TENANT_ID-User
|
|
8
8
|
// → audit-fähig.
|
|
9
|
-
import { selectAllTenants, selectMembershipsOfUser, selectUserByEmail, } from "../db/queries/seed-context.js";
|
|
9
|
+
import { selectAllTenants, selectMembershipsOfUser, selectTemplateResources, selectUserByEmail, } from "../db/queries/seed-context.js";
|
|
10
10
|
import { createSystemUser, SYSTEM_TENANT_ID } from "../engine/index.js";
|
|
11
11
|
import { parseStringArrayJson } from "../utils/parse-string-array-json.js";
|
|
12
12
|
/** Builder: gibt eine factory-function zurück die der Runner pro-Migration
|
|
@@ -65,6 +65,20 @@ export function createSeedMigrationContext(args) {
|
|
|
65
65
|
const rows = await selectAllTenants(args.dbRunner);
|
|
66
66
|
return rows.map((r) => ({ id: r.id, name: r.name, tenantKey: r.tenant_key }));
|
|
67
67
|
},
|
|
68
|
+
findTemplateResources: async (filter) => {
|
|
69
|
+
const rows = await selectTemplateResources(args.dbRunner, {
|
|
70
|
+
...filter,
|
|
71
|
+
tenantId: filter?.tenantId ?? SYSTEM_TENANT_ID,
|
|
72
|
+
});
|
|
73
|
+
return rows.map((r) => ({
|
|
74
|
+
id: r.id,
|
|
75
|
+
tenantId: r.tenant_id,
|
|
76
|
+
slug: r.slug,
|
|
77
|
+
kind: r.kind,
|
|
78
|
+
locale: r.locale,
|
|
79
|
+
status: r.status,
|
|
80
|
+
}));
|
|
81
|
+
},
|
|
68
82
|
db: args.dbRunner,
|
|
69
83
|
};
|
|
70
84
|
}
|
package/dist/es-ops/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { type CreateSeedMigrationContextArgs, createSeedMigrationContext, } from "./context.js";
|
|
2
2
|
export { createEsOperationsTable, type EsOperationAppliedBy, type EsOperationType, esOperationsTable, } from "./operations-schema.js";
|
|
3
3
|
export { type RunPendingSeedMigrationsArgs, type RunPendingSeedMigrationsResult, runPendingSeedMigrations, } from "./runner.js";
|
|
4
|
-
export type { SeedMembershipRow, SeedMigration, SeedMigrationContext, SeedTenantRow, SeedUserRow, } from "./types.js";
|
|
4
|
+
export type { SeedMembershipRow, SeedMigration, SeedMigrationContext, SeedTemplateResourceFilter, SeedTemplateResourceRow, SeedTenantRow, SeedUserRow, } from "./types.js";
|
package/dist/es-ops/types.d.ts
CHANGED
|
@@ -51,6 +51,23 @@ export type SeedTenantRow = {
|
|
|
51
51
|
readonly name: string;
|
|
52
52
|
readonly tenantKey: string;
|
|
53
53
|
};
|
|
54
|
+
/** Read-shape of a template-resolver row as exposed to seeds. */
|
|
55
|
+
export type SeedTemplateResourceRow = {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly tenantId: string;
|
|
58
|
+
readonly slug: string;
|
|
59
|
+
readonly kind: string;
|
|
60
|
+
readonly locale: string;
|
|
61
|
+
readonly status: string;
|
|
62
|
+
};
|
|
63
|
+
export type SeedTemplateResourceFilter = {
|
|
64
|
+
/** Defaults to the system tenant, where seeded system templates live. */
|
|
65
|
+
readonly tenantId?: TenantId;
|
|
66
|
+
readonly slug?: string;
|
|
67
|
+
readonly kind?: string;
|
|
68
|
+
readonly status?: string;
|
|
69
|
+
readonly locale?: string;
|
|
70
|
+
};
|
|
54
71
|
export type SeedMigrationContext = {
|
|
55
72
|
/** Event-Store-konformer Write via existing write-handler. System-User
|
|
56
73
|
* als Executor bypassed Access-Check (Standard-Seed-Pattern). Events
|
|
@@ -84,6 +101,9 @@ export type SeedMigrationContext = {
|
|
|
84
101
|
readonly findUserByEmail: (email: string) => Promise<SeedUserRow | null>;
|
|
85
102
|
readonly findMembershipsOfUser: (userId: string) => Promise<readonly SeedMembershipRow[]>;
|
|
86
103
|
readonly findTenants: () => Promise<readonly SeedTenantRow[]>;
|
|
104
|
+
/** Template rows ordered by slug, locale. Empty list when the
|
|
105
|
+
* template-resolver feature (its table) is not present. */
|
|
106
|
+
readonly findTemplateResources: (filter?: SeedTemplateResourceFilter) => Promise<readonly SeedTemplateResourceRow[]>;
|
|
87
107
|
/** Escape-Hatch — direkter DB-Zugang. Nur für READ-only Lookups die der
|
|
88
108
|
* Context nicht standard-mäßig anbietet. WRITES via systemWriteAs!
|
|
89
109
|
* Type ist DbRunner (Connection oder aktive Tx) weil der Runner den
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { type EventMetadata, type StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
4
|
import type { DbRunner } from "../db/index.js";
|
|
4
5
|
import type { TenantId } from "../engine/types/index.js";
|
|
5
6
|
export type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
3
|
-
|
|
4
|
-
// `Temporal` TYPE this file's other Temporal.Instant annotations resolve
|
|
5
|
-
// against (StoredEvent.createdAt et al. — importing the bare name here
|
|
6
|
-
// conflicts with that ambient type, see #1438). Bun doesn't expose Temporal
|
|
7
|
-
// as a runtime value on globalThis, so the un-aliased call below crashed
|
|
8
|
-
// with "Temporal is not defined" outside boot paths that install it (#1480).
|
|
9
|
-
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
4
|
import { requestContext } from "../api/request-context.js";
|
|
11
5
|
import { encryptEventPayloadPii } from "../crypto/event-pii.js";
|
|
12
6
|
import { constraintOf, isUniqueViolation } from "../db/pg-error.js";
|
|
@@ -150,18 +144,11 @@ async function insertSubsequentEvent(db, event, newVersion, eventVersion, seedCr
|
|
|
150
144
|
if (!row)
|
|
151
145
|
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
152
146
|
const createdAt = row.created_at instanceof Date
|
|
153
|
-
?
|
|
154
|
-
:
|
|
147
|
+
? Temporal.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
148
|
+
: Temporal.Instant.from(row.created_at);
|
|
155
149
|
return {
|
|
156
150
|
id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
|
|
157
|
-
|
|
158
|
-
// at runtime, two separate .d.ts sources (temporal-polyfill's bundled
|
|
159
|
-
// types vs the ambient temporal-spec global) disagree on a couple of
|
|
160
|
-
// method-overload signatures (until/equals argument types), so TS treats
|
|
161
|
-
// them as nominally distinct. Safe: this value is only ever consumed as
|
|
162
|
-
// a Temporal.Instant (comparisons, .toString(), arithmetic), never
|
|
163
|
-
// narrowed on the specific overload shape that differs.
|
|
164
|
-
createdAt: createdAt,
|
|
151
|
+
createdAt,
|
|
165
152
|
};
|
|
166
153
|
}
|
|
167
154
|
function buildStoredEvent(event, newVersion, eventVersion, row) {
|
|
@@ -9,6 +9,7 @@ export { createRebuildDeadLetterTable, listRebuildDeadLetters, type RebuildDeadL
|
|
|
9
9
|
export { toStoredEvent } from "./row-to-stored-event.js";
|
|
10
10
|
export { assertSeedModeNotInProduction, isSeedModeEnabled, runSeedWritesAt, SEED_MODE_ENV, SeedModeDisabledError, } from "./seed-clock.js";
|
|
11
11
|
export { createSnapshotsTable, type LoadAggregateWithSnapshotOptions, type LoadAggregateWithSnapshotResult, loadAggregateWithSnapshot, loadLatestSnapshot, type SaveSnapshotArgs, type Snapshot, type SnapshotReducer, saveSnapshot, snapshotsTable, } from "./snapshot.js";
|
|
12
|
+
export { type AppendEventInTenantDbOptions, appendEventInTenantDb, getStreamVersionInTenantDb, } from "./tenant-db-append.js";
|
|
12
13
|
export { AGGREGATE_TRANSFER_STREAM_TYPE, AGGREGATE_TRANSFERRED_EVENT_TYPE, type TransferAggregateStreamsArgs, transferAggregateStreams, } from "./transfer.js";
|
|
13
14
|
export { type EventUpcasters, makeUpcastCtx, type UpcasterErrorPolicy, type UpcastOptions, upcastStoredEvent, upcastStoredEvents, } from "./upcaster.js";
|
|
14
15
|
export { createUpcasterDeadLetterTable, type DeadLetterRow, listDeadLetters, recordUpcasterDeadLetter, upcasterDeadLetterTable, } from "./upcaster-dead-letter.js";
|
|
@@ -9,6 +9,7 @@ export { createRebuildDeadLetterTable, listRebuildDeadLetters, rebuildDeadLetter
|
|
|
9
9
|
export { toStoredEvent } from "./row-to-stored-event.js";
|
|
10
10
|
export { assertSeedModeNotInProduction, isSeedModeEnabled, runSeedWritesAt, SEED_MODE_ENV, SeedModeDisabledError, } from "./seed-clock.js";
|
|
11
11
|
export { createSnapshotsTable, loadAggregateWithSnapshot, loadLatestSnapshot, saveSnapshot, snapshotsTable, } from "./snapshot.js";
|
|
12
|
+
export { appendEventInTenantDb, getStreamVersionInTenantDb, } from "./tenant-db-append.js";
|
|
12
13
|
export { AGGREGATE_TRANSFER_STREAM_TYPE, AGGREGATE_TRANSFERRED_EVENT_TYPE, transferAggregateStreams, } from "./transfer.js";
|
|
13
14
|
export { makeUpcastCtx, upcastStoredEvent, upcastStoredEvents, } from "./upcaster.js";
|
|
14
15
|
export { createUpcasterDeadLetterTable, listDeadLetters, recordUpcasterDeadLetter, upcasterDeadLetterTable, } from "./upcaster-dead-letter.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
2
|
+
import type { Registry } from "../engine/types/index.js";
|
|
3
|
+
import { type EventToAppend, type StoredEvent } from "./event-store.js";
|
|
4
|
+
export type AppendEventInTenantDbOptions = {
|
|
5
|
+
readonly registry?: Registry;
|
|
6
|
+
};
|
|
7
|
+
export declare function appendEventInTenantDb(tenantDb: TenantDb, event: Omit<EventToAppend, "tenantId">, options?: AppendEventInTenantDbOptions): Promise<StoredEvent>;
|
|
8
|
+
export declare function getStreamVersionInTenantDb(tenantDb: TenantDb, aggregateId: string): Promise<number>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
2
|
+
import { runProjectionsForEvent } from "../pipeline/projections-runner.js";
|
|
3
|
+
import { append, getStreamVersion } from "./event-store.js";
|
|
4
|
+
// Appends on the connection or transaction the TenantDb is bound to. The tenant is always the
|
|
5
|
+
// TenantDb's own, so a caller cannot write into another tenant's stream; a TenantDb the framework
|
|
6
|
+
// did not build (or a fail-closed systemScope guard) is rejected by tenantDbRunner.
|
|
7
|
+
export async function appendEventInTenantDb(tenantDb, event, options = {}) {
|
|
8
|
+
const runner = tenantDbRunner(tenantDb);
|
|
9
|
+
const stored = await append(runner, { ...event, tenantId: tenantDb.tenantId });
|
|
10
|
+
if (options.registry)
|
|
11
|
+
await runProjectionsForEvent(stored, options.registry, runner);
|
|
12
|
+
return stored;
|
|
13
|
+
}
|
|
14
|
+
export async function getStreamVersionInTenantDb(tenantDb, aggregateId) {
|
|
15
|
+
return getStreamVersion(tenantDbRunner(tenantDb), aggregateId, tenantDb.tenantId);
|
|
16
|
+
}
|
|
@@ -90,8 +90,10 @@ function pushDashboardPanelKeys(out, panel) {
|
|
|
90
90
|
if (panel.kind === "custom")
|
|
91
91
|
return;
|
|
92
92
|
pushKey(out, panel.label);
|
|
93
|
-
if (panel.kind === "chart")
|
|
93
|
+
if (panel.kind === "chart" || panel.kind === "stat-group")
|
|
94
94
|
pushKey(out, panel.subtitle);
|
|
95
|
+
if (panel.kind === "chart")
|
|
96
|
+
pushDashboardChartRangeKeys(out, panel);
|
|
95
97
|
if (panel.kind === "chart" ||
|
|
96
98
|
panel.kind === "list" ||
|
|
97
99
|
panel.kind === "feed" ||
|
|
@@ -111,6 +113,10 @@ function pushDashboardPanelKeys(out, panel) {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
}
|
|
116
|
+
function pushDashboardChartRangeKeys(out, panel) {
|
|
117
|
+
for (const option of panel.ranges?.options ?? [])
|
|
118
|
+
pushKey(out, option.label);
|
|
119
|
+
}
|
|
114
120
|
function pushDashboardFilterKeys(out, filter) {
|
|
115
121
|
pushKey(out, filter.label);
|
|
116
122
|
if (filter.allLabel !== undefined)
|