@cosmicdrift/kumiko-framework 0.157.2 → 0.159.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/package.json +7 -2
- package/src/__tests__/consumer-cli.integration.test.ts +110 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +16 -1
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/jwt.test.ts +150 -1
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +66 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/api-constants.ts +4 -0
- package/src/api/auth-middleware.ts +48 -59
- package/src/api/auth-routes.ts +83 -17
- package/src/api/index.ts +8 -4
- package/src/api/jwt.ts +148 -7
- package/src/api/pii-leak-guard.ts +5 -2
- package/src/api/server.ts +19 -5
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/query.ts +34 -2
- package/src/consumer-cli.ts +87 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
- package/src/crypto/blind-index.ts +8 -4
- package/src/crypto/event-pii.ts +1 -0
- package/src/crypto/pii-field-encryption.ts +49 -15
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +305 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
- package/src/db/blind-index-cleanup.ts +3 -1
- package/src/db/connection.ts +3 -11
- package/src/db/encryption.ts +2 -3
- package/src/db/entity-table-meta-types.ts +92 -0
- package/src/db/entity-table-meta.ts +16 -90
- package/src/db/queries/backfill-pii.ts +1 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/store-table.test.ts +12 -0
- package/src/engine/boot-validator/action-wiring.ts +1 -1
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/entity-list-screens.ts +1 -1
- package/src/engine/boot-validator/gdpr-storage.ts +0 -112
- package/src/engine/boot-validator/index.ts +3 -9
- package/src/engine/boot-validator/screens.ts +1 -1
- package/src/engine/define-feature.ts +1 -0
- package/src/engine/define-handler.ts +10 -91
- package/src/engine/entity-handlers.ts +15 -27
- package/src/engine/feature-builder-state.ts +3 -0
- package/src/engine/feature-config-events-jobs.ts +1 -1
- package/src/engine/feature-entity-handlers.ts +1 -1
- package/src/engine/feature-ui-extensions.ts +5 -1
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +14 -0
- package/src/engine/index.ts +2 -2
- package/src/engine/ownership.ts +22 -76
- package/src/engine/registry-validate.ts +1 -1
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/define-handler.ts +94 -0
- package/src/engine/types/entity-handlers.ts +30 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +45 -0
- package/src/engine/types/fields.ts +19 -31
- package/src/engine/types/handlers.ts +7 -26
- package/src/engine/types/hooks.ts +1 -15
- package/src/engine/types/http-route.ts +1 -72
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +34 -9
- package/src/engine/types/ownership.ts +83 -0
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +0 -46
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -129
- package/src/entrypoint/index.ts +2 -2
- package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
- package/src/event-store/event-store.ts +28 -32
- package/src/event-store/events-schema.ts +1 -10
- package/src/event-store/index.ts +3 -2
- package/src/event-store/types.ts +22 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/file-handle.ts +2 -19
- package/src/i18n/required-surface-keys.ts +1 -1
- package/src/logging/types.ts +1 -7
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -58
- package/src/pipeline/__tests__/dispatcher.test.ts +38 -1
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
- package/src/pipeline/dispatch-shared.ts +12 -2
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +28 -3
- package/src/pipeline/event-dispatcher-admin.ts +4 -0
- package/src/pipeline/event-dispatcher-delivery.ts +29 -3
- package/src/pipeline/event-dispatcher.ts +27 -1
- package/src/pipeline/system-hooks.ts +7 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/envelope-cipher.ts +13 -39
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/testing/__tests__/late-bound.test.ts +25 -0
- package/src/testing/__tests__/wait-for.test.ts +53 -0
- package/src/testing/boot-validator-fixture.ts +1 -1
- package/src/testing/file-provider-contract.ts +84 -0
- package/src/testing/handler-context.ts +1 -1
- package/src/testing/index.ts +1 -0
- package/src/time/geo-tz.ts +1 -32
- package/src/ui-types/index.ts +7 -7
|
@@ -3,6 +3,7 @@ import { QnTypes, qualifyEntityName } from "../qualified-name";
|
|
|
3
3
|
import type { ClaimKeyDefinition, FeatureDefinition } from "../types";
|
|
4
4
|
import { validateActionWiring, validateFieldWiring } from "./action-wiring";
|
|
5
5
|
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext";
|
|
6
|
+
import { validateFeatureBootChecks } from "./boot-check";
|
|
6
7
|
import {
|
|
7
8
|
validateCircularDeps,
|
|
8
9
|
validateConfigKeyAllowPerRequest,
|
|
@@ -30,12 +31,7 @@ import {
|
|
|
30
31
|
validateTransitions,
|
|
31
32
|
} from "./entity-handler";
|
|
32
33
|
import { validateEntityListScreens } from "./entity-list-screens";
|
|
33
|
-
import {
|
|
34
|
-
validateGdprHookCompleteness,
|
|
35
|
-
validateGdprPiiHookCoverage,
|
|
36
|
-
validateGdprStoragePersistence,
|
|
37
|
-
validateTenantDataHookCoverage,
|
|
38
|
-
} from "./gdpr-storage";
|
|
34
|
+
import { validateGdprStoragePersistence } from "./gdpr-storage";
|
|
39
35
|
import { validateI18nSurfaceKeys } from "./i18n-keys";
|
|
40
36
|
import {
|
|
41
37
|
collectKnownRoles,
|
|
@@ -198,9 +194,7 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
|
|
|
198
194
|
validateEntityListScreens(features);
|
|
199
195
|
validateExtensionPreSaveWiring(features);
|
|
200
196
|
validateGdprStoragePersistence(features);
|
|
201
|
-
|
|
202
|
-
validateGdprPiiHookCoverage(features);
|
|
203
|
-
validateTenantDataHookCoverage(features);
|
|
197
|
+
validateFeatureBootChecks(features);
|
|
204
198
|
|
|
205
199
|
if (hasEncryptedFields) {
|
|
206
200
|
// Availability check, not env-presence: eagerly building the keyring
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { rowMetaFieldNames } from "../../db/table-builder";
|
|
8
8
|
import { qualifyEntityName } from "../qualified-name";
|
|
9
9
|
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops";
|
|
10
|
+
import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
|
|
10
11
|
import type { FeatureDefinition } from "../types";
|
|
11
12
|
import type {
|
|
12
13
|
DashboardCustomPanel,
|
|
@@ -20,7 +21,6 @@ import type {
|
|
|
20
21
|
ScreenDefinition,
|
|
21
22
|
ToolbarAction,
|
|
22
23
|
} from "../types/screen";
|
|
23
|
-
import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../types/screen";
|
|
24
24
|
|
|
25
25
|
// --- Screen validation ---
|
|
26
26
|
//
|
|
@@ -136,6 +136,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
136
136
|
extensionSelectors: state.extensionSelectors,
|
|
137
137
|
exposedApis: state.exposedApis,
|
|
138
138
|
usedApis: state.usedApis,
|
|
139
|
+
bootChecks: state.bootChecks,
|
|
139
140
|
referenceData: state.referenceData,
|
|
140
141
|
events: state.events,
|
|
141
142
|
eventMigrations: state.eventMigrations,
|
|
@@ -1,81 +1,18 @@
|
|
|
1
1
|
import type { ZodType, z } from "zod";
|
|
2
2
|
import type { ContainsSecret } from "../secrets/types";
|
|
3
3
|
import { runPipeline } from "./run-pipeline";
|
|
4
|
+
import type { HandlerContext, KumikoEventTypeMap, WriteEvent, WriteResult } from "./types";
|
|
4
5
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
RateLimitOption,
|
|
10
|
-
WriteEvent,
|
|
11
|
-
WriteResult,
|
|
12
|
-
} from "./types";
|
|
13
|
-
import type { PipelineDef } from "./types/step";
|
|
6
|
+
QueryHandlerDefinition,
|
|
7
|
+
WriteHandlerDefinition,
|
|
8
|
+
WriteHandlerInput,
|
|
9
|
+
} from "./types/define-handler";
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// substitute TMap at the USE-site (the caller's compile context, where
|
|
21
|
-
// the augmentation is visible); generic-type-aliases substitute at the
|
|
22
|
-
// definition-site (framework's compile, where the augmentation isn't
|
|
23
|
-
// visible) and collapse `keyof TMap` to `never`. See the spike-findings
|
|
24
|
-
// memory for the empirical proof.
|
|
25
|
-
//
|
|
26
|
-
// Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
|
|
27
|
-
// (step-pipeline). A `perform` is compiled to a handler-function at
|
|
28
|
-
// definition time; the dispatcher only ever sees `handler`.
|
|
29
|
-
|
|
30
|
-
export type WriteHandlerDefinition<
|
|
31
|
-
TName extends string = string,
|
|
32
|
-
TSchema extends ZodType = ZodType,
|
|
33
|
-
TData = unknown,
|
|
34
|
-
TMap extends object = KumikoEventTypeMap,
|
|
35
|
-
> = {
|
|
36
|
-
readonly name: TName;
|
|
37
|
-
readonly schema: TSchema;
|
|
38
|
-
readonly access?: AccessRule;
|
|
39
|
-
readonly unsafeSkipTransitionGuard?: boolean;
|
|
40
|
-
readonly rateLimit?: RateLimitOption;
|
|
41
|
-
readonly handler: (
|
|
42
|
-
event: WriteEvent<z.infer<TSchema>>,
|
|
43
|
-
context: HandlerContext<TMap>,
|
|
44
|
-
) => Promise<WriteResult<TData>>;
|
|
45
|
-
// Preserved when the author wrote a `perform` block — the original
|
|
46
|
-
// PipelineDef. Designer/AI/AST tools read this when present; the
|
|
47
|
-
// dispatcher ignores it and just calls `handler`. Absent on free-form
|
|
48
|
-
// handlers.
|
|
49
|
-
readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Author-facing input — accepts either the free-form `handler` or the
|
|
53
|
-
// pipeline-form `perform`. defineWriteHandler narrows them to the
|
|
54
|
-
// canonical WriteHandlerDefinition shape.
|
|
55
|
-
export type WriteHandlerInput<
|
|
56
|
-
TName extends string = string,
|
|
57
|
-
TSchema extends ZodType = ZodType,
|
|
58
|
-
TData = unknown,
|
|
59
|
-
TMap extends object = KumikoEventTypeMap,
|
|
60
|
-
> = {
|
|
61
|
-
readonly name: TName;
|
|
62
|
-
readonly schema: TSchema;
|
|
63
|
-
readonly access?: AccessRule;
|
|
64
|
-
readonly unsafeSkipTransitionGuard?: boolean;
|
|
65
|
-
readonly rateLimit?: RateLimitOption;
|
|
66
|
-
} & (
|
|
67
|
-
| {
|
|
68
|
-
readonly handler: (
|
|
69
|
-
event: WriteEvent<z.infer<TSchema>>,
|
|
70
|
-
context: HandlerContext<TMap>,
|
|
71
|
-
) => Promise<WriteResult<TData>>;
|
|
72
|
-
readonly perform?: never;
|
|
73
|
-
}
|
|
74
|
-
| {
|
|
75
|
-
readonly perform: PipelineDef<z.infer<TSchema>, TData>;
|
|
76
|
-
readonly handler?: never;
|
|
77
|
-
}
|
|
78
|
-
);
|
|
11
|
+
export type {
|
|
12
|
+
QueryHandlerDefinition,
|
|
13
|
+
WriteHandlerDefinition,
|
|
14
|
+
WriteHandlerInput,
|
|
15
|
+
} from "./types/define-handler";
|
|
79
16
|
|
|
80
17
|
export function defineWriteHandler<
|
|
81
18
|
const TName extends string,
|
|
@@ -160,24 +97,6 @@ export function defineWriteHandler<
|
|
|
160
97
|
return { ...base, handler: def.handler };
|
|
161
98
|
}
|
|
162
99
|
|
|
163
|
-
// --- Query Handler Definition ---
|
|
164
|
-
|
|
165
|
-
export type QueryHandlerDefinition<
|
|
166
|
-
TName extends string = string,
|
|
167
|
-
TSchema extends ZodType = ZodType,
|
|
168
|
-
TResult = unknown,
|
|
169
|
-
TMap extends object = KumikoEventTypeMap,
|
|
170
|
-
> = {
|
|
171
|
-
readonly name: TName;
|
|
172
|
-
readonly schema: TSchema;
|
|
173
|
-
readonly access?: AccessRule;
|
|
174
|
-
readonly rateLimit?: RateLimitOption;
|
|
175
|
-
readonly handler: (
|
|
176
|
-
query: QueryEvent<z.infer<TSchema>>,
|
|
177
|
-
context: HandlerContext<TMap>,
|
|
178
|
-
) => Promise<TResult>;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
100
|
export function defineQueryHandler<
|
|
182
101
|
const TName extends string,
|
|
183
102
|
TSchema extends ZodType,
|
|
@@ -18,6 +18,21 @@ import type {
|
|
|
18
18
|
QueryHandlerDef,
|
|
19
19
|
WriteHandlerDef,
|
|
20
20
|
} from "./types";
|
|
21
|
+
import type {
|
|
22
|
+
EntityCrudRegistrar,
|
|
23
|
+
EntityCrudVerb,
|
|
24
|
+
EntityHandlerOptions,
|
|
25
|
+
EntityQueryHandlerOptions,
|
|
26
|
+
RegisterEntityCrudOptions,
|
|
27
|
+
} from "./types/entity-handlers";
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
EntityCrudRegistrar,
|
|
31
|
+
EntityCrudVerb,
|
|
32
|
+
EntityHandlerOptions,
|
|
33
|
+
EntityQueryHandlerOptions,
|
|
34
|
+
RegisterEntityCrudOptions,
|
|
35
|
+
} from "./types/entity-handlers";
|
|
21
36
|
|
|
22
37
|
// Convention-based handler factories for event-sourced aggregates.
|
|
23
38
|
//
|
|
@@ -330,16 +345,6 @@ export function defineEntityQueryHandler(
|
|
|
330
345
|
// would need a Branded-EntityDefinition with `softDelete: true` literal
|
|
331
346
|
// — feasible but not yet wired; the runtime guard catches misuse.
|
|
332
347
|
|
|
333
|
-
type EntityHandlerOptions = { readonly access?: AccessRule };
|
|
334
|
-
type EntityQueryHandlerOptions = EntityHandlerOptions & {
|
|
335
|
-
/** Reads across every tenant instead of the caller's own — for a
|
|
336
|
-
* SystemAdmin-only operator inspector over an otherwise tenant-scoped
|
|
337
|
-
* entity. Scope this to the ONE handler that needs it rather than making
|
|
338
|
-
* the whole feature r.systemScope(), which would drop tenant isolation
|
|
339
|
-
* from every other handler the feature registers too. */
|
|
340
|
-
readonly crossTenant?: boolean;
|
|
341
|
-
};
|
|
342
|
-
|
|
343
348
|
// @wrapper-known semantic-alias
|
|
344
349
|
export function defineEntityCreateHandler(
|
|
345
350
|
entityName: string,
|
|
@@ -446,23 +451,6 @@ export function defineProjectionQueryHandler(
|
|
|
446
451
|
};
|
|
447
452
|
}
|
|
448
453
|
|
|
449
|
-
type EntityCrudVerb = "create" | "update" | "delete" | "restore" | "list" | "detail";
|
|
450
|
-
|
|
451
|
-
export type RegisterEntityCrudOptions = {
|
|
452
|
-
readonly write?: EntityHandlerOptions;
|
|
453
|
-
readonly read?: EntityQueryHandlerOptions;
|
|
454
|
-
readonly verbs?: Partial<Record<EntityCrudVerb, boolean>>;
|
|
455
|
-
/** Default true. Set false when the entity was already registered (e.g. before r.relation). */
|
|
456
|
-
readonly registerEntity?: boolean;
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
/** Minimal registrar surface — keeps entity-handlers free of define-feature imports. */
|
|
460
|
-
export type EntityCrudRegistrar = {
|
|
461
|
-
entity(name: string, definition: EntityDefinition): unknown;
|
|
462
|
-
writeHandler(def: WriteHandlerDef): unknown;
|
|
463
|
-
queryHandler(def: QueryHandlerDef): unknown;
|
|
464
|
-
};
|
|
465
|
-
|
|
466
454
|
function defaultCrudVerbs(entity: EntityDefinition): Record<EntityCrudVerb, boolean> {
|
|
467
455
|
return {
|
|
468
456
|
create: true,
|
|
@@ -2,6 +2,7 @@ import type { ZodType, z } from "zod";
|
|
|
2
2
|
import { LifecycleHookTypes } from "./constants";
|
|
3
3
|
import type {
|
|
4
4
|
AuthClaimsFn,
|
|
5
|
+
BootCheckFn,
|
|
5
6
|
ClaimKeyDefinition,
|
|
6
7
|
ConfigKeyDefinition,
|
|
7
8
|
ConfigSeedDef,
|
|
@@ -79,6 +80,7 @@ export type FeatureBuilderState = {
|
|
|
79
80
|
extensionSelectors: ExtensionSelectorDef[];
|
|
80
81
|
exposedApis: Set<string>;
|
|
81
82
|
usedApis: Set<string>;
|
|
83
|
+
bootChecks: BootCheckFn[];
|
|
82
84
|
referenceData: ReferenceDataDef[];
|
|
83
85
|
handlerEntityMappings: Record<string, string>;
|
|
84
86
|
metrics: Record<string, FeatureMetricDef>;
|
|
@@ -137,6 +139,7 @@ export function createInitialFeatureBuilderState(): FeatureBuilderState {
|
|
|
137
139
|
extensionSelectors: [],
|
|
138
140
|
exposedApis: new Set(),
|
|
139
141
|
usedApis: new Set(),
|
|
142
|
+
bootChecks: [],
|
|
140
143
|
referenceData: [],
|
|
141
144
|
handlerEntityMappings: {},
|
|
142
145
|
metrics: {},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ZodObject, type ZodType, type z } from "zod";
|
|
2
2
|
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
3
|
+
import { resolveName } from "./handler-helpers";
|
|
3
4
|
import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
|
|
4
5
|
import { QnTypes, qn, toKebab } from "./qualified-name";
|
|
5
6
|
import type {
|
|
@@ -26,7 +27,6 @@ import type {
|
|
|
26
27
|
SecretOptions,
|
|
27
28
|
TranslationsDef,
|
|
28
29
|
} from "./types";
|
|
29
|
-
import { resolveName } from "./types/handlers";
|
|
30
30
|
|
|
31
31
|
// Builds config/secrets/claims/events/jobs/notifications registrar methods.
|
|
32
32
|
export function buildConfigEventsJobsMethods<TName extends string>(
|
|
@@ -4,6 +4,7 @@ import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-ha
|
|
|
4
4
|
import type { RegisterEntityCrudOptions } from "./entity-handlers";
|
|
5
5
|
import { registerEntityCrud } from "./entity-handlers";
|
|
6
6
|
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
7
|
+
import { resolveName } from "./handler-helpers";
|
|
7
8
|
import { splitNamedDefinition } from "./object-form";
|
|
8
9
|
import type {
|
|
9
10
|
AccessRule,
|
|
@@ -16,7 +17,6 @@ import type {
|
|
|
16
17
|
RelationDefinition,
|
|
17
18
|
WriteHandlerFn,
|
|
18
19
|
} from "./types";
|
|
19
|
-
import { resolveName } from "./types/handlers";
|
|
20
20
|
import type { PipelineDef } from "./types/step";
|
|
21
21
|
|
|
22
22
|
const CRUD_VERBS = new Set(["create", "update", "delete"]);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { EntityTableMeta } from "../db/entity-table-meta";
|
|
2
2
|
import { LifecycleHookTypes } from "./constants";
|
|
3
3
|
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
4
|
+
import { resolveName } from "./handler-helpers";
|
|
4
5
|
import { isKebabSegment, toKebab } from "./qualified-name";
|
|
5
6
|
import type {
|
|
7
|
+
BootCheckFn,
|
|
6
8
|
EntityProjectionExtension,
|
|
7
9
|
HookPhase,
|
|
8
10
|
LifecycleHookFn,
|
|
@@ -22,7 +24,6 @@ import type {
|
|
|
22
24
|
ValidationHookFn,
|
|
23
25
|
} from "./types";
|
|
24
26
|
import { HookPhases } from "./types";
|
|
25
|
-
import { resolveName } from "./types/handlers";
|
|
26
27
|
import type { HttpRouteDefinition } from "./types/http-route";
|
|
27
28
|
import type { NavDefinition } from "./types/nav";
|
|
28
29
|
import type { ScreenDefinition } from "./types/screen";
|
|
@@ -255,6 +256,9 @@ export function buildUiExtensionsMethods<TName extends string>(
|
|
|
255
256
|
usesApi(apiName: string): void {
|
|
256
257
|
state.usedApis.add(apiName);
|
|
257
258
|
},
|
|
259
|
+
bootCheck(fn: BootCheckFn): void {
|
|
260
|
+
state.bootChecks.push(fn);
|
|
261
|
+
},
|
|
258
262
|
projection(definition: ProjectionDefinition): void {
|
|
259
263
|
// Reject names that would blow up at registry-boot when we qualify them.
|
|
260
264
|
// Catch it at the registration site so the stack trace points at the
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AnyFileFieldDef, FieldDefinition } from "./types/fields";
|
|
2
|
+
|
|
3
|
+
// --- Currency ---
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CURRENCIES = [
|
|
6
|
+
"EUR",
|
|
7
|
+
"USD",
|
|
8
|
+
"GBP",
|
|
9
|
+
"CHF",
|
|
10
|
+
"JPY",
|
|
11
|
+
"SEK",
|
|
12
|
+
"NOK",
|
|
13
|
+
"DKK",
|
|
14
|
+
"PLN",
|
|
15
|
+
"CZK",
|
|
16
|
+
"CAD",
|
|
17
|
+
"AUD",
|
|
18
|
+
"NZD",
|
|
19
|
+
"CNY",
|
|
20
|
+
"INR",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export function isFileField(field: FieldDefinition | undefined): field is AnyFileFieldDef {
|
|
24
|
+
if (!field) return false;
|
|
25
|
+
return (
|
|
26
|
+
field.type === "file" ||
|
|
27
|
+
field.type === "image" ||
|
|
28
|
+
field.type === "files" ||
|
|
29
|
+
field.type === "images"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NameOrRef, WriteResult } from "./types/handlers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Override the success-side `data` of a WriteResult while forwarding the
|
|
5
|
+
* failure half untouched. Useful for handlers that delegate to the
|
|
6
|
+
* event-store executor (which returns a SaveContext / DeleteContext
|
|
7
|
+
* envelope) but want to keep their own response shape — caller contract
|
|
8
|
+
* stays flat instead of leaking the executor's internals.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const result = await executor.delete({ id }, user, db);
|
|
12
|
+
* return withResponseData(result, { userId, tenantId });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* On failure the same WriteFailure instance is returned — the error
|
|
16
|
+
* object round-trips without any wrapping, so the dispatcher / HTTP layer
|
|
17
|
+
* still read the original error code + httpStatus + i18nKey.
|
|
18
|
+
*/
|
|
19
|
+
export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
|
|
20
|
+
if (!result.isSuccess) return result;
|
|
21
|
+
return { isSuccess: true, data };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveName(ref: NameOrRef): string {
|
|
25
|
+
return typeof ref === "string" ? ref : ref.name;
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// --- Hook Phases ---
|
|
2
|
+
//
|
|
3
|
+
// inTransaction: Hook runs inside the DB transaction. Failures roll back
|
|
4
|
+
// the entire write. Use for: DB-based side-effects (counter updates,
|
|
5
|
+
// dependent entity writes).
|
|
6
|
+
//
|
|
7
|
+
// afterCommit (default): Hook runs after the transaction commits. Failures
|
|
8
|
+
// are logged but don't affect the write. Use for: external systems
|
|
9
|
+
// (SSE broadcast, search index, email, webhooks).
|
|
10
|
+
|
|
11
|
+
export const HookPhases = {
|
|
12
|
+
inTransaction: "inTransaction",
|
|
13
|
+
afterCommit: "afterCommit",
|
|
14
|
+
} as const;
|
package/src/engine/index.ts
CHANGED
|
@@ -177,6 +177,7 @@ export {
|
|
|
177
177
|
checkWriteFieldRoles,
|
|
178
178
|
filterReadFields,
|
|
179
179
|
} from "./field-access";
|
|
180
|
+
export { resolveName, withResponseData } from "./handler-helpers";
|
|
180
181
|
// findForbiddenMembershipRole/isForbiddenMembershipRole/
|
|
181
182
|
// stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
|
|
182
183
|
// apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
|
|
@@ -203,6 +204,7 @@ export { runsInLane } from "./run-in";
|
|
|
203
204
|
export type { StepListOutcome } from "./run-pipeline";
|
|
204
205
|
export { runPipeline, runStepList } from "./run-pipeline";
|
|
205
206
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder";
|
|
207
|
+
export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./screen-helpers";
|
|
206
208
|
export type { TransitionGraph } from "./state-machine";
|
|
207
209
|
export { defineTransitions, guardTransition } from "./state-machine";
|
|
208
210
|
export {
|
|
@@ -385,9 +387,7 @@ export type {
|
|
|
385
387
|
WriteResult,
|
|
386
388
|
} from "./types";
|
|
387
389
|
export { DEFAULT_CURRENCIES, HookPhases } from "./types";
|
|
388
|
-
export { resolveName, withResponseData } from "./types/handlers";
|
|
389
390
|
export { isSystemTenant, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
|
|
390
|
-
export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./types/screen";
|
|
391
391
|
export type {
|
|
392
392
|
PipelineBuildCtx,
|
|
393
393
|
PipelineCtx,
|
package/src/engine/ownership.ts
CHANGED
|
@@ -20,64 +20,28 @@
|
|
|
20
20
|
import { toSnakeCase } from "../db/table-builder";
|
|
21
21
|
import type { SessionUser } from "./types";
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export type FromRuleKind = "user" | "claim";
|
|
46
|
-
|
|
47
|
-
export type FromRule = {
|
|
48
|
-
readonly kind: "from";
|
|
49
|
-
readonly refKind: FromRuleKind;
|
|
50
|
-
// For "user:id" → "id"; for "user:tenantId" → "tenantId".
|
|
51
|
-
// For "claim:<featureName>:<key>" → "<featureName>:<key>" (the full QN,
|
|
52
|
-
// which is exactly the key under which the JWT stores the value).
|
|
53
|
-
readonly refPath: string;
|
|
54
|
-
// Row-column to match against. For claim rules defaults to the claim's
|
|
55
|
-
// shortName (second segment of the claim QN). For user-rules the column
|
|
56
|
-
// is always explicit.
|
|
57
|
-
readonly column: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Context passed to a WhereRule escape-hatch. The author returns a SqlFragment
|
|
61
|
-
// whose placeholders start at `paramStart` ($N, $N+1, ...); the framework
|
|
62
|
-
// concatenates the fragment into the larger query.
|
|
63
|
-
export type WhereRuleContext<TTable = unknown> = {
|
|
64
|
-
readonly table: TTable;
|
|
65
|
-
readonly tableName: string;
|
|
66
|
-
readonly paramStart: number;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export type WhereRule<TTable = unknown> = {
|
|
70
|
-
readonly kind: "where";
|
|
71
|
-
readonly where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// "all" collapses to a primitive so map authors can write `Admin: "all"`
|
|
75
|
-
// without importing a helper.
|
|
76
|
-
export type OwnershipRule = "all" | FromRule | WhereRule;
|
|
77
|
-
|
|
78
|
-
// Per-role map: every key is a role name, value is the rule that role
|
|
79
|
-
// satisfies to pass the access check.
|
|
80
|
-
export type OwnershipMap = Readonly<Record<string, OwnershipRule>>;
|
|
23
|
+
// Types live in engine/types/ownership.ts (no runtime dependency); re-exported
|
|
24
|
+
// here for backwards compatibility with existing importers of this file.
|
|
25
|
+
export type {
|
|
26
|
+
FromRule,
|
|
27
|
+
FromRuleKind,
|
|
28
|
+
OwnershipClause,
|
|
29
|
+
OwnershipMap,
|
|
30
|
+
OwnershipRef,
|
|
31
|
+
OwnershipRule,
|
|
32
|
+
SqlFragment,
|
|
33
|
+
WhereRule,
|
|
34
|
+
WhereRuleContext,
|
|
35
|
+
} from "./types/ownership";
|
|
36
|
+
|
|
37
|
+
import type {
|
|
38
|
+
FromRule,
|
|
39
|
+
OwnershipClause,
|
|
40
|
+
OwnershipMap,
|
|
41
|
+
OwnershipRef,
|
|
42
|
+
OwnershipRule,
|
|
43
|
+
SqlFragment,
|
|
44
|
+
} from "./types/ownership";
|
|
81
45
|
|
|
82
46
|
// Parse an OwnershipRef into kind + resolved path + default column.
|
|
83
47
|
// Throws on malformed input so the error surfaces at `from()`-call-site (in
|
|
@@ -254,24 +218,6 @@ export function userCanCreateFieldRow(
|
|
|
254
218
|
return false;
|
|
255
219
|
}
|
|
256
220
|
|
|
257
|
-
// Result of buildOwnershipClause. The discriminant lets the caller handle
|
|
258
|
-
// the three outcomes without inspecting SQL internals:
|
|
259
|
-
//
|
|
260
|
-
// "pass" → user is unrestricted. Run the query as-is.
|
|
261
|
-
// "empty" → user has a role mapped but no rule accepts any row (missing
|
|
262
|
-
// claim, empty array, role not in map). Skip the DB call entirely
|
|
263
|
-
// — returning [] is equivalent and avoids a pointless roundtrip.
|
|
264
|
-
// "sql" → apply the parameterised fragment as an AND on the query.
|
|
265
|
-
// Caller is responsible for renumbering placeholders when
|
|
266
|
-
// concatenating with other fragments (see `shiftParams` below).
|
|
267
|
-
//
|
|
268
|
-
// "empty" vs. "pass" is the critical distinction for a safe default:
|
|
269
|
-
// undefined/pass = allow, empty = deny-by-construction.
|
|
270
|
-
export type OwnershipClause =
|
|
271
|
-
| { readonly kind: "pass" }
|
|
272
|
-
| { readonly kind: "empty" }
|
|
273
|
-
| { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
|
|
274
|
-
|
|
275
221
|
const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
|
|
276
222
|
const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
|
|
277
223
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { configureEventPiiCatalog } from "../crypto/event-pii";
|
|
2
|
+
import { resolveName } from "./handler-helpers";
|
|
2
3
|
import type { RegistryState } from "./registry-state";
|
|
3
4
|
import { buildImplicitProjection, hasFieldAccessRules, qualify } from "./registry-state";
|
|
4
5
|
import {
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
} from "./soft-delete-cleanup";
|
|
12
13
|
import type { EventPiiFields, EventUpcastFn, FeatureDefinition } from "./types";
|
|
13
14
|
import { HookPhases } from "./types";
|
|
14
|
-
import { resolveName } from "./types/handlers";
|
|
15
15
|
|
|
16
16
|
function allHandlerQns(state: RegistryState): ReadonlySet<string> {
|
|
17
17
|
return new Set([...state.writeHandlerMap.keys(), ...state.queryHandlerMap.keys()]);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditExtensionSection,
|
|
3
|
+
EditFieldSpec,
|
|
4
|
+
EditSectionSpec,
|
|
5
|
+
FieldCondition,
|
|
6
|
+
FormatSpec,
|
|
7
|
+
ListColumnSpec,
|
|
8
|
+
} from "./types/screen";
|
|
9
|
+
|
|
10
|
+
export function isExtensionEditSection(section: EditSectionSpec): section is EditExtensionSection {
|
|
11
|
+
return section.kind === "extension";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
|
|
15
|
+
// authors who branch on the three FieldRenderer variants without manual
|
|
16
|
+
// "format" in renderer checks.
|
|
17
|
+
export function isFormatSpec(r: unknown): r is FormatSpec {
|
|
18
|
+
return typeof r === "object" && r !== null && "format" in r && typeof r.format === "string";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Collapse the string-shorthand into the object form. Both the boot-validator
|
|
22
|
+
// and (later) ui-core's view-model builder iterate over fields/columns — the
|
|
23
|
+
// helper keeps that loop from growing two branches everywhere.
|
|
24
|
+
export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
|
|
25
|
+
const col = typeof c === "string" ? { field: c } : c;
|
|
26
|
+
if (
|
|
27
|
+
typeof process !== "undefined" &&
|
|
28
|
+
process.env.NODE_ENV !== "production" &&
|
|
29
|
+
col.renderer !== undefined &&
|
|
30
|
+
typeof col.renderer === "function"
|
|
31
|
+
) {
|
|
32
|
+
// biome-ignore lint/suspicious/noConsole: dev-only warning
|
|
33
|
+
console.warn(
|
|
34
|
+
`[kumiko] normalizeListColumn: Feld "${col.field}" hat einen Funktions-Renderer — dieser wird von JSON.stringify verworfen. Bitte auf FormatSpec ({ format: "..." }) migrieren.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return col;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Evaluates a declarative FieldCondition against the current row/form
|
|
41
|
+
* values. THE single implementation — renderer (row-action visibility),
|
|
42
|
+
* headless view-model (visible/readOnly/required) and render-edit
|
|
43
|
+
* (form-condition closures) reuse it; three hand-rolled copies had
|
|
44
|
+
* already drifted in shape. */
|
|
45
|
+
export function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
|
|
46
|
+
if (typeof cond === "boolean") return cond;
|
|
47
|
+
const val = values[cond.field];
|
|
48
|
+
if ("eq" in cond) return val === cond.eq;
|
|
49
|
+
return val !== cond.ne;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
|
|
53
|
+
return typeof f === "string" ? { field: f } : f;
|
|
54
|
+
}
|
|
@@ -48,8 +48,9 @@ export type EffectiveFeaturesResolver = ((tenantId: TenantId) => ReadonlySet<str
|
|
|
48
48
|
* Signup-Datum des Tenants live (tenant.inserted_at) und returnt true wenn
|
|
49
49
|
* der Tenant im Trial-Fenster ist UND das Feature zum Trial-Tier gehört.
|
|
50
50
|
* Async + nicht im Boot-Cache, weil der Trial zeit-abgeleitet ist (ändert
|
|
51
|
-
* sich zwischen Requests).
|
|
52
|
-
*
|
|
51
|
+
* sich zwischen Requests). Konsultiert an 3 Stellen: den 2 Dispatch-Gate-
|
|
52
|
+
* Aufrufstellen (checkFeatureEnabled) UND ctx.hasFeature im Handler-Body
|
|
53
|
+
* (fällt darauf zurück, wenn der synchrone Feature-Set-Check negativ ist).
|
|
53
54
|
*/
|
|
54
55
|
readonly trialGate?: TrialGate;
|
|
55
56
|
};
|