@cosmicdrift/kumiko-framework 0.2.1 → 0.2.3
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/CHANGELOG.md +52 -0
- package/package.json +4 -3
- package/src/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- package/src/db/table-builder.ts +18 -1
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
- package/src/engine/boot-validator.ts +276 -0
- package/src/engine/define-feature.ts +39 -0
- package/src/engine/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +15 -5
- package/src/engine/feature-ast/extractors.ts +40 -0
- package/src/engine/feature-ast/parse.ts +6 -0
- package/src/engine/feature-ast/patterns.ts +22 -0
- package/src/engine/feature-ast/render.ts +14 -0
- package/src/engine/index.ts +21 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +5 -0
- package/src/engine/pattern-library/library.ts +36 -0
- package/src/engine/schema-builder.ts +8 -0
- package/src/engine/types/feature.ts +51 -0
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/index.ts +3 -0
- package/src/files/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +34 -6
- package/src/files/zip-stream.ts +251 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Hook-Signatur-Types für die EXT_USER_DATA-Extension (DSGVO Art. 15+17+20).
|
|
2
|
+
//
|
|
3
|
+
// Sprint 1.9 Z1: Bisher gibt der Boot-Validator JEDES Hook-Shape durch
|
|
4
|
+
// — useExtension(EXT_USER_DATA, "X", { export: ... }) wird nicht gegen
|
|
5
|
+
// eine erwartete Signatur geprüft. Diese Types sind die canonical
|
|
6
|
+
// Schema-Sicht; Sprint 2 user-data-rights wird sie via
|
|
7
|
+
// `r.extendsRegistrar(EXT_USER_DATA, { hooks: ... })`-Doku exposen.
|
|
8
|
+
//
|
|
9
|
+
// Boot-Time-Schape-Check (Runtime) ist orthogonal und kommt in Sprint
|
|
10
|
+
// 2 wenn die exportRunner-/forgetRunner-Pipelines stehen — bis dahin
|
|
11
|
+
// sind diese Types Compile-Time-Hints für App-Authors, keine Runtime-
|
|
12
|
+
// Validation.
|
|
13
|
+
//
|
|
14
|
+
// Siehe docs/plans/datenschutz/user-data-rights.md.
|
|
15
|
+
|
|
16
|
+
import type { DbRunner } from "../../db/connection";
|
|
17
|
+
import type { TenantId } from "../types";
|
|
18
|
+
|
|
19
|
+
// SessionUser.id ist plattformweit `string` (kein Brand-Type). Wenn
|
|
20
|
+
// jemals ein UserId-Brand eingefuehrt wird, ersetzt man hier den
|
|
21
|
+
// inline-Type — andere Codebase-Stellen nutzen denselben Pfad.
|
|
22
|
+
type UserId = string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strategie für den Forget-Pfad pro Entity:
|
|
26
|
+
* - "delete": Row physisch entfernen (Profil, Eigene Notizen, Sessions).
|
|
27
|
+
* - "anonymize": User-Reference auf null + Display-Felder auf
|
|
28
|
+
* "[Geloescht]" — typisch für geteilte Daten (Tasks,
|
|
29
|
+
* Comments) damit andere User die History nicht verlieren.
|
|
30
|
+
*
|
|
31
|
+
* Cleanup-Job (Sprint 2 data-retention) entscheidet pro Entity über
|
|
32
|
+
* Retention-Policy, welche Strategie greift. blockDelete-Entries lösen
|
|
33
|
+
* IMMER `anonymize` aus damit Aufbewahrungs-Pflicht respektiert wird.
|
|
34
|
+
*/
|
|
35
|
+
export type UserDataDeleteStrategy = "delete" | "anonymize";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Context-Snapshot der dem Hook übergeben wird. Sprint 2 erweitert
|
|
39
|
+
* das ggf. um cancel-/timeout-Marker; aktuell minimaler Schnitt.
|
|
40
|
+
*
|
|
41
|
+
* `db` ist `DbRunner` (DbConnection | DbTx) damit der Cleanup-Runner
|
|
42
|
+
* (S2.U5b) den Hook in einer Per-User-Sub-Tx callen kann. Hooks die
|
|
43
|
+
* raw-DB-Operationen machen funktionieren auf beiden Shapes via
|
|
44
|
+
* Drizzle's polymorphem select/insert/update/delete-Chain.
|
|
45
|
+
*/
|
|
46
|
+
export interface UserDataHookCtx {
|
|
47
|
+
readonly db: DbRunner;
|
|
48
|
+
readonly tenantId: TenantId;
|
|
49
|
+
readonly userId: UserId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pro Feature/Entity-Snippet das im Export-Bundle landet. Sprint 2
|
|
54
|
+
* orchestriert die JSON-Serialisierung; Hooks geben Plain-Records.
|
|
55
|
+
*/
|
|
56
|
+
export interface UserDataExportSnippet {
|
|
57
|
+
readonly entity: string;
|
|
58
|
+
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
59
|
+
/**
|
|
60
|
+
* Optional: signed-URLs für File-Refs. user-data-rights packt sie
|
|
61
|
+
* separat ins ZIP unter `files/`. Andere Hooks lassen das leer.
|
|
62
|
+
*/
|
|
63
|
+
readonly fileRefs?: ReadonlyArray<{
|
|
64
|
+
readonly fileRefId: string;
|
|
65
|
+
readonly storageKey: string;
|
|
66
|
+
readonly fileName: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Export-Hook: Sammelt alle Daten einer Entity die zu einem User
|
|
72
|
+
* gehören. Wird im Daten-Export-Job pro registrierter Entity einmal
|
|
73
|
+
* aufgerufen. Idempotent — kann mehrfach aufgerufen werden ohne
|
|
74
|
+
* Side-Effects.
|
|
75
|
+
*
|
|
76
|
+
* Sprint 2 user-data-rights ruft das via Iteration über alle
|
|
77
|
+
* `r.useExtension(EXT_USER_DATA, ...)`-Registrierungen.
|
|
78
|
+
*/
|
|
79
|
+
export type UserDataExportHook = (ctx: UserDataHookCtx) => Promise<UserDataExportSnippet | null>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Forget-Hook: Löscht oder anonymisiert die Entity-Rows die zu einem
|
|
83
|
+
* User gehören. Strategy kommt vom Cleanup-Job (kann per Entity
|
|
84
|
+
* unterschiedlich sein wegen Retention-Policy).
|
|
85
|
+
*
|
|
86
|
+
* Idempotent — wenn der Job zweimal läuft (Crash-Recovery), darf der
|
|
87
|
+
* Hook nicht crashen.
|
|
88
|
+
*/
|
|
89
|
+
export type UserDataDeleteHook = (
|
|
90
|
+
ctx: UserDataHookCtx,
|
|
91
|
+
strategy: UserDataDeleteStrategy,
|
|
92
|
+
) => Promise<void>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Komplette Hook-Tafel für EXT_USER_DATA. Sprint 2 user-data-rights
|
|
96
|
+
* deklariert das via `r.extendsRegistrar(EXT_USER_DATA, { hooks: ... })`,
|
|
97
|
+
* konsumierende Features liefern beide Hooks via
|
|
98
|
+
* `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })`.
|
|
99
|
+
*
|
|
100
|
+
* Kein Hook ist optional — beide MÜSSEN registriert sein. Boot-Check
|
|
101
|
+
* (Sprint 2) prüft das.
|
|
102
|
+
*/
|
|
103
|
+
export interface UserDataExtensionHooks {
|
|
104
|
+
readonly export: UserDataExportHook;
|
|
105
|
+
readonly delete: UserDataDeleteHook;
|
|
106
|
+
}
|
package/src/engine/factories.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BigIntFieldDef,
|
|
2
3
|
BooleanFieldDef,
|
|
3
4
|
DateFieldDef,
|
|
4
5
|
EmbeddedFieldDef,
|
|
5
6
|
EntityDefinition,
|
|
7
|
+
EntityIndexDef,
|
|
6
8
|
FieldsMap,
|
|
7
9
|
FileFieldDef,
|
|
8
10
|
FilesFieldDef,
|
|
@@ -13,6 +15,7 @@ import type {
|
|
|
13
15
|
MoneyFieldDef,
|
|
14
16
|
MultiSelectFieldDef,
|
|
15
17
|
NumberFieldDef,
|
|
18
|
+
RetentionDef,
|
|
16
19
|
SelectFieldDef,
|
|
17
20
|
TextFieldDef,
|
|
18
21
|
TimestampFieldDef,
|
|
@@ -126,6 +129,16 @@ export function createNumberField<R extends true | false = false>(
|
|
|
126
129
|
} as NumberFieldDef & { required: R };
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
export function createBigIntField<R extends true | false = false>(
|
|
133
|
+
overrides?: Partial<Omit<BigIntFieldDef, "type" | "required">> & { required?: R },
|
|
134
|
+
): BigIntFieldDef & { required: R } {
|
|
135
|
+
return {
|
|
136
|
+
type: "bigInt",
|
|
137
|
+
required: false,
|
|
138
|
+
...overrides,
|
|
139
|
+
} as BigIntFieldDef & { required: R };
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
export function createMoneyField<R extends true | false = false>(
|
|
130
143
|
overrides?: Partial<Omit<MoneyFieldDef, "type" | "required">> & { required?: R },
|
|
131
144
|
): MoneyFieldDef & { required: R } {
|
|
@@ -309,13 +322,10 @@ export function createEntity<F>(def: {
|
|
|
309
322
|
readonly searchWeight?: number;
|
|
310
323
|
readonly defaultCurrency?: string;
|
|
311
324
|
readonly transitions?: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;
|
|
312
|
-
readonly indexes?: readonly
|
|
313
|
-
readonly columns: readonly [string, ...string[]];
|
|
314
|
-
readonly unique?: boolean;
|
|
315
|
-
readonly name?: string;
|
|
316
|
-
}[];
|
|
325
|
+
readonly indexes?: readonly EntityIndexDef[];
|
|
317
326
|
readonly idType?: "serial" | "uuid";
|
|
318
327
|
readonly access?: EntityDefinition["access"];
|
|
328
|
+
readonly retention?: RetentionDef;
|
|
319
329
|
}): F extends FieldsMap ? EntityDefinition<F> : never {
|
|
320
330
|
return {
|
|
321
331
|
softDelete: false,
|
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
EntityHookPattern,
|
|
51
51
|
EntityPattern,
|
|
52
52
|
EventMigrationPattern,
|
|
53
|
+
ExposesApiPattern,
|
|
53
54
|
ExtendsRegistrarPattern,
|
|
54
55
|
HookPattern,
|
|
55
56
|
HttpRoutePattern,
|
|
@@ -72,6 +73,7 @@ import type {
|
|
|
72
73
|
ToggleablePattern,
|
|
73
74
|
TranslationsPattern,
|
|
74
75
|
UseExtensionPattern,
|
|
76
|
+
UsesApiPattern,
|
|
75
77
|
WorkspacePattern,
|
|
76
78
|
WriteHandlerPattern,
|
|
77
79
|
} from "./patterns";
|
|
@@ -2560,3 +2562,41 @@ export function extractExtendsRegistrar(
|
|
|
2560
2562
|
defBody: sourceLocationFromNode(defArg, sourceFile),
|
|
2561
2563
|
});
|
|
2562
2564
|
}
|
|
2565
|
+
|
|
2566
|
+
export function extractUsesApi(
|
|
2567
|
+
call: CallExpression,
|
|
2568
|
+
sourceFile: SourceFile,
|
|
2569
|
+
): ExtractOutput<UsesApiPattern> {
|
|
2570
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
2571
|
+
if (!arg) {
|
|
2572
|
+
return fail(
|
|
2573
|
+
"usesApi",
|
|
2574
|
+
sourceLocationFromNode(call, sourceFile),
|
|
2575
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
return ok({
|
|
2579
|
+
kind: "usesApi",
|
|
2580
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
2581
|
+
apiName: arg.getLiteralValue(),
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
export function extractExposesApi(
|
|
2586
|
+
call: CallExpression,
|
|
2587
|
+
sourceFile: SourceFile,
|
|
2588
|
+
): ExtractOutput<ExposesApiPattern> {
|
|
2589
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
2590
|
+
if (!arg) {
|
|
2591
|
+
return fail(
|
|
2592
|
+
"exposesApi",
|
|
2593
|
+
sourceLocationFromNode(call, sourceFile),
|
|
2594
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
return ok({
|
|
2598
|
+
kind: "exposesApi",
|
|
2599
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
2600
|
+
apiName: arg.getLiteralValue(),
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
extractEntity,
|
|
34
34
|
extractEntityHook,
|
|
35
35
|
extractEventMigration,
|
|
36
|
+
extractExposesApi,
|
|
36
37
|
extractExtendsRegistrar,
|
|
37
38
|
extractHook,
|
|
38
39
|
extractHttpRoute,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
extractToggleable,
|
|
55
56
|
extractTranslations,
|
|
56
57
|
extractUseExtension,
|
|
58
|
+
extractUsesApi,
|
|
57
59
|
extractWorkspace,
|
|
58
60
|
extractWriteHandler,
|
|
59
61
|
} from "./extractors";
|
|
@@ -346,6 +348,10 @@ function dispatchExtractor(
|
|
|
346
348
|
// Round 5 — opaque patterns
|
|
347
349
|
case "extendsRegistrar":
|
|
348
350
|
return extractExtendsRegistrar(call, sourceFile);
|
|
351
|
+
case "usesApi":
|
|
352
|
+
return extractUsesApi(call, sourceFile);
|
|
353
|
+
case "exposesApi":
|
|
354
|
+
return extractExposesApi(call, sourceFile);
|
|
349
355
|
// Unknown method — UnknownPattern signal so Designer/AI surface it
|
|
350
356
|
// as "custom call" without losing the source location.
|
|
351
357
|
default:
|
|
@@ -318,6 +318,24 @@ export type ExtendsRegistrarPattern = {
|
|
|
318
318
|
readonly defBody: SourceLocation;
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
+
// r.usesApi("a.b") — declarative cross-feature handler-ID dependency.
|
|
322
|
+
// Boot-validation throws if no other feature exposes the handler. Single
|
|
323
|
+
// string argument; pattern is purely declarative.
|
|
324
|
+
export type UsesApiPattern = {
|
|
325
|
+
readonly kind: "usesApi";
|
|
326
|
+
readonly source: SourceLocation;
|
|
327
|
+
readonly apiName: string;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// r.exposesApi("a.b") — declarative announcement that this feature
|
|
331
|
+
// provides a handler matching the cross-feature contract `a.b`. Single
|
|
332
|
+
// string argument; pattern is purely declarative.
|
|
333
|
+
export type ExposesApiPattern = {
|
|
334
|
+
readonly kind: "exposesApi";
|
|
335
|
+
readonly source: SourceLocation;
|
|
336
|
+
readonly apiName: string;
|
|
337
|
+
};
|
|
338
|
+
|
|
321
339
|
// =============================================================================
|
|
322
340
|
// Catch-all — r.* calls the visitor doesn't recognise. Designer renders
|
|
323
341
|
// "unknown call (cannot edit)", AI patcher leaves them unchanged. When
|
|
@@ -354,6 +372,8 @@ export type FeaturePattern =
|
|
|
354
372
|
| ReferenceDataPattern
|
|
355
373
|
| ReadsConfigPattern
|
|
356
374
|
| UseExtensionPattern
|
|
375
|
+
| UsesApiPattern
|
|
376
|
+
| ExposesApiPattern
|
|
357
377
|
// Mixed
|
|
358
378
|
| ScreenPattern
|
|
359
379
|
| WriteHandlerPattern
|
|
@@ -406,6 +426,8 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
406
426
|
case "referenceData":
|
|
407
427
|
case "readsConfig":
|
|
408
428
|
case "useExtension":
|
|
429
|
+
case "usesApi":
|
|
430
|
+
case "exposesApi":
|
|
409
431
|
return "static";
|
|
410
432
|
case "screen":
|
|
411
433
|
case "writeHandler":
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
EntityHookPattern,
|
|
25
25
|
EntityPattern,
|
|
26
26
|
EventMigrationPattern,
|
|
27
|
+
ExposesApiPattern,
|
|
27
28
|
ExtendsRegistrarPattern,
|
|
28
29
|
FeaturePattern,
|
|
29
30
|
HookPattern,
|
|
@@ -47,6 +48,7 @@ import type {
|
|
|
47
48
|
TranslationsPattern,
|
|
48
49
|
UnknownPattern,
|
|
49
50
|
UseExtensionPattern,
|
|
51
|
+
UsesApiPattern,
|
|
50
52
|
WorkspacePattern,
|
|
51
53
|
WriteHandlerPattern,
|
|
52
54
|
} from "./patterns";
|
|
@@ -122,6 +124,10 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
122
124
|
return renderEventMigration(pattern);
|
|
123
125
|
case "extendsRegistrar":
|
|
124
126
|
return renderExtendsRegistrar(pattern);
|
|
127
|
+
case "usesApi":
|
|
128
|
+
return renderUsesApi(pattern);
|
|
129
|
+
case "exposesApi":
|
|
130
|
+
return renderExposesApi(pattern);
|
|
125
131
|
case "unknown":
|
|
126
132
|
return renderUnknown(pattern);
|
|
127
133
|
default: {
|
|
@@ -473,6 +479,14 @@ function renderExtendsRegistrar(p: ExtendsRegistrarPattern): string {
|
|
|
473
479
|
return `r.extendsRegistrar(${JSON.stringify(p.extensionName)}, ${p.defBody.raw});`;
|
|
474
480
|
}
|
|
475
481
|
|
|
482
|
+
function renderUsesApi(p: UsesApiPattern): string {
|
|
483
|
+
return `r.usesApi(${JSON.stringify(p.apiName)});`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderExposesApi(p: ExposesApiPattern): string {
|
|
487
|
+
return `r.exposesApi(${JSON.stringify(p.apiName)});`;
|
|
488
|
+
}
|
|
489
|
+
|
|
476
490
|
function renderUnknown(p: UnknownPattern): string {
|
|
477
491
|
// Round-trip preservation only: emit the raw call text from the
|
|
478
492
|
// SourceLocation so the rendered file stays semantically identical
|
package/src/engine/index.ts
CHANGED
|
@@ -41,7 +41,25 @@ export {
|
|
|
41
41
|
} from "./entity-handlers";
|
|
42
42
|
export type { EmitCtx } from "./event-helpers";
|
|
43
43
|
export { emitEvent, typedPayload } from "./event-helpers";
|
|
44
|
+
export type { KumikoExtensionName } from "./extension-names";
|
|
44
45
|
export {
|
|
46
|
+
EXT_EXTERNAL_RESOURCE,
|
|
47
|
+
EXT_INFRA_RESOURCE,
|
|
48
|
+
EXT_SEARCH_ADAPTER,
|
|
49
|
+
EXT_STORAGE_PROVIDER,
|
|
50
|
+
EXT_TENANT_DATA,
|
|
51
|
+
EXT_USER_DATA,
|
|
52
|
+
} from "./extension-names";
|
|
53
|
+
export type {
|
|
54
|
+
UserDataDeleteHook,
|
|
55
|
+
UserDataDeleteStrategy,
|
|
56
|
+
UserDataExportHook,
|
|
57
|
+
UserDataExportSnippet,
|
|
58
|
+
UserDataExtensionHooks,
|
|
59
|
+
UserDataHookCtx,
|
|
60
|
+
} from "./extensions/user-data";
|
|
61
|
+
export {
|
|
62
|
+
createBigIntField,
|
|
45
63
|
createBooleanField,
|
|
46
64
|
createDateField,
|
|
47
65
|
createEmbeddedField,
|
|
@@ -144,6 +162,7 @@ export type {
|
|
|
144
162
|
AuthClaimsFn,
|
|
145
163
|
AuthClaimsHookDef,
|
|
146
164
|
BelongsToRelation,
|
|
165
|
+
BigIntFieldDef,
|
|
147
166
|
BooleanFieldDef,
|
|
148
167
|
CamelToKebab,
|
|
149
168
|
ClaimKeyDefinition,
|
|
@@ -220,6 +239,7 @@ export type {
|
|
|
220
239
|
NotifyPriority,
|
|
221
240
|
NumberFieldDef,
|
|
222
241
|
OnDeleteStrategy,
|
|
242
|
+
PiiAnnotations,
|
|
223
243
|
PlatformComponent,
|
|
224
244
|
PostDeleteHookFn,
|
|
225
245
|
PostSaveHookFn,
|
|
@@ -234,6 +254,7 @@ export type {
|
|
|
234
254
|
QueryHandlerFn,
|
|
235
255
|
Registry,
|
|
236
256
|
RelationDefinition,
|
|
257
|
+
RetentionDef,
|
|
237
258
|
RowAction,
|
|
238
259
|
SaveContext,
|
|
239
260
|
ScreenDefinition,
|
|
@@ -55,6 +55,8 @@ const ALL_KINDS: readonly FeaturePatternKind[] = [
|
|
|
55
55
|
"defineEvent",
|
|
56
56
|
"eventMigration",
|
|
57
57
|
"extendsRegistrar",
|
|
58
|
+
"usesApi",
|
|
59
|
+
"exposesApi",
|
|
58
60
|
"unknown",
|
|
59
61
|
];
|
|
60
62
|
|
|
@@ -340,6 +342,9 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
340
342
|
};
|
|
341
343
|
case "unknown":
|
|
342
344
|
return { kind, source: PLACEHOLDER_LOC, methodName: "x" };
|
|
345
|
+
case "usesApi":
|
|
346
|
+
case "exposesApi":
|
|
347
|
+
return { kind, source: PLACEHOLDER_LOC, apiName: "demo.api" };
|
|
343
348
|
default: {
|
|
344
349
|
const _exhaustive: never = kind;
|
|
345
350
|
return _exhaustive;
|
|
@@ -1020,6 +1020,40 @@ const extendsRegistrarSchema: PatternFormSchema = {
|
|
|
1020
1020
|
],
|
|
1021
1021
|
};
|
|
1022
1022
|
|
|
1023
|
+
const usesApiSchema: PatternFormSchema = {
|
|
1024
|
+
kind: "usesApi",
|
|
1025
|
+
label: { en: "Uses API", de: "Nutzt API" },
|
|
1026
|
+
summary: {
|
|
1027
|
+
en: "Cross-feature handler-ID dependency. Boot fails if no other feature exposes it.",
|
|
1028
|
+
},
|
|
1029
|
+
category: "advanced",
|
|
1030
|
+
editability: "static",
|
|
1031
|
+
fields: [
|
|
1032
|
+
{
|
|
1033
|
+
path: "apiName",
|
|
1034
|
+
label: { en: "API name", de: "API-Name" },
|
|
1035
|
+
input: "text",
|
|
1036
|
+
required: true,
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const exposesApiSchema: PatternFormSchema = {
|
|
1042
|
+
kind: "exposesApi",
|
|
1043
|
+
label: { en: "Exposes API", de: "Stellt API bereit" },
|
|
1044
|
+
summary: { en: "Declares this feature provides a handler matching the cross-feature contract." },
|
|
1045
|
+
category: "advanced",
|
|
1046
|
+
editability: "static",
|
|
1047
|
+
fields: [
|
|
1048
|
+
{
|
|
1049
|
+
path: "apiName",
|
|
1050
|
+
label: { en: "API name", de: "API-Name" },
|
|
1051
|
+
input: "text",
|
|
1052
|
+
required: true,
|
|
1053
|
+
},
|
|
1054
|
+
],
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1023
1057
|
const unknownSchema: PatternFormSchema = {
|
|
1024
1058
|
kind: "unknown",
|
|
1025
1059
|
label: { en: "Unknown call", de: "Unbekannter Call" },
|
|
@@ -1077,6 +1111,8 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
1077
1111
|
defineEvent: defineEventSchema,
|
|
1078
1112
|
eventMigration: eventMigrationSchema,
|
|
1079
1113
|
extendsRegistrar: extendsRegistrarSchema,
|
|
1114
|
+
usesApi: usesApiSchema,
|
|
1115
|
+
exposesApi: exposesApiSchema,
|
|
1080
1116
|
unknown: unknownSchema,
|
|
1081
1117
|
};
|
|
1082
1118
|
|
|
@@ -60,6 +60,14 @@ function fieldToZod(field: FieldDefinition, currencies: readonly string[]): z.Zo
|
|
|
60
60
|
const schema = z.number();
|
|
61
61
|
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
62
62
|
}
|
|
63
|
+
case "bigInt": {
|
|
64
|
+
// JS-`number`-Round-trip via mode:"number"; sicher bis 2^53.
|
|
65
|
+
// safe-integer-Cap ist explizit damit ein Caller, der einen
|
|
66
|
+
// Float reinwirft (z.B. parseFloat-Bug), beim Insert sofort
|
|
67
|
+
// failed statt silent-Truncation zu kassieren.
|
|
68
|
+
const schema = z.number().int().safe();
|
|
69
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
70
|
+
}
|
|
63
71
|
case "money": {
|
|
64
72
|
const [first, ...rest] = currencies;
|
|
65
73
|
if (!first) throw new Error("No currencies configured");
|
|
@@ -164,6 +164,21 @@ export type FeatureDefinition = {
|
|
|
164
164
|
readonly jobs: Readonly<Record<string, JobDefinition>>;
|
|
165
165
|
readonly registrarExtensions: Readonly<Record<string, RegistrarExtensionDef>>;
|
|
166
166
|
readonly extensionUsages: readonly RegistrarExtensionRegistration[];
|
|
167
|
+
/**
|
|
168
|
+
* Cross-feature API names this feature exposes via `r.exposesApi(name)`.
|
|
169
|
+
* Pure Marker-Deklaration — die echte Implementation wird als
|
|
170
|
+
* Query-/Write-Handler unter dem QN-Pattern registriert (z.B.
|
|
171
|
+
* `compliance-profiles:query:effective-profile`). Boot-Validator prüft
|
|
172
|
+
* dass jedes `r.usesApi(name)` einen passenden Exposer hier findet —
|
|
173
|
+
* Tippfehler oder Drop-Refactorings werden zu Boot-Fail statt Runtime-Crash.
|
|
174
|
+
*/
|
|
175
|
+
readonly exposedApis: ReadonlySet<string>;
|
|
176
|
+
/**
|
|
177
|
+
* Cross-feature API names this feature calls. Pflicht-Boot-Check:
|
|
178
|
+
* jeder Eintrag muss in `exposedApis` irgendeines Features auftauchen
|
|
179
|
+
* UND das Provider-Feature muss in requires/optionalRequires sein.
|
|
180
|
+
*/
|
|
181
|
+
readonly usedApis: ReadonlySet<string>;
|
|
167
182
|
readonly referenceData: readonly ReferenceDataDef[];
|
|
168
183
|
readonly notifications: Readonly<Record<string, NotificationDefinition>>;
|
|
169
184
|
readonly events: Readonly<Record<string, EventDef>>;
|
|
@@ -360,6 +375,42 @@ export type FeatureRegistrar<TFeature extends string = string> = {
|
|
|
360
375
|
|
|
361
376
|
useExtension(extensionName: string, entity: NameOrRef, options?: Record<string, unknown>): void;
|
|
362
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
|
|
380
|
+
* unter dem genannten Namen bereit. Die eigentliche Implementation
|
|
381
|
+
* wird separat als Query- oder Write-Handler unter dem QN-Pattern
|
|
382
|
+
* registriert; `r.exposesApi` ist reine Boot-Check-Surface.
|
|
383
|
+
*
|
|
384
|
+
* Boot-Validator prüft, dass jedes `r.usesApi(name)` einen passenden
|
|
385
|
+
* Exposer findet, dass das Exposer-Feature in requires/optionalRequires
|
|
386
|
+
* gelisted ist und dass kein API-Name doppelt exposed wird.
|
|
387
|
+
*
|
|
388
|
+
* ```ts
|
|
389
|
+
* defineFeature("compliance-profiles", (r) => {
|
|
390
|
+
* r.exposesApi("compliance.forTenant");
|
|
391
|
+
* r.queryHandler({
|
|
392
|
+
* name: "compliance:query:for-tenant",
|
|
393
|
+
* // ... echte Implementation
|
|
394
|
+
* });
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
exposesApi(apiName: string): void;
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Declares that this feature calls a cross-feature API. Boot-Validator
|
|
402
|
+
* checkt dass irgendein anderes Feature `r.exposesApi(apiName)` macht
|
|
403
|
+
* und dass dieses Feature `r.requires/optionalRequires` darauf hat.
|
|
404
|
+
*
|
|
405
|
+
* ```ts
|
|
406
|
+
* defineFeature("user-data-rights", (r) => {
|
|
407
|
+
* r.requires("compliance-profiles");
|
|
408
|
+
* r.usesApi("compliance.forTenant");
|
|
409
|
+
* });
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
usesApi(apiName: string): void;
|
|
413
|
+
|
|
363
414
|
// Declare a metric. Short name (without kumiko_<feature>_ prefix) — Framework
|
|
364
415
|
// qualifies it on boot. Validation (snake_case + typ-suffix) runs at boot.
|
|
365
416
|
// Usage at runtime: ctx.metrics.inc("created_total", { status: "new" }).
|