@cosmicdrift/kumiko-framework 0.27.0 → 0.28.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/package.json +1 -1
- package/src/api/server.ts +7 -1
- package/src/engine/__tests__/engine.test.ts +29 -0
- package/src/engine/define-feature.ts +14 -0
- package/src/engine/feature-ast/extractors/index.ts +1 -0
- package/src/engine/feature-ast/extractors/round1.ts +22 -0
- package/src/engine/feature-ast/parse.ts +3 -0
- package/src/engine/feature-ast/patch.ts +3 -0
- package/src/engine/feature-ast/patcher.ts +5 -0
- package/src/engine/feature-ast/patterns.ts +8 -0
- package/src/engine/feature-ast/render.ts +7 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +3 -0
- package/src/engine/pattern-library/library.ts +19 -0
- package/src/engine/system-user.ts +10 -2
- package/src/engine/types/feature.ts +6 -0
- package/src/entrypoint/index.ts +8 -3
- package/src/files/feature.ts +3 -0
- package/src/stack/test-stack.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
package/src/api/server.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
wrapRedisClient,
|
|
24
24
|
} from "../observability";
|
|
25
25
|
import type { DispatcherOptions } from "../pipeline/dispatcher";
|
|
26
|
-
import { createDispatcher } from "../pipeline/dispatcher";
|
|
26
|
+
import { createDispatcher, type Dispatcher } from "../pipeline/dispatcher";
|
|
27
27
|
import { SHARED_INSTANCE_SENTINEL } from "../pipeline/event-consumer-state";
|
|
28
28
|
import type { EventDedup } from "../pipeline/event-dedup";
|
|
29
29
|
import type { EventConsumer, EventDispatcher } from "../pipeline/event-dispatcher";
|
|
@@ -195,6 +195,11 @@ export type KumikoServer = {
|
|
|
195
195
|
jwt: JwtHelper;
|
|
196
196
|
sseBroker: SseBroker;
|
|
197
197
|
observability: ObservabilityProvider;
|
|
198
|
+
// The command-dispatcher behind /api/* — same idempotency/jobRunner/
|
|
199
|
+
// lifecycle wiring as HTTP-dispatched writes. For dispatching outside
|
|
200
|
+
// the HTTP pipeline, e.g. provider-webhook routes that authenticate
|
|
201
|
+
// via signature instead of JWT (subscription-stripe et al.).
|
|
202
|
+
dispatcher: Dispatcher;
|
|
198
203
|
// Present when at least one consumer is wired and context.db is a
|
|
199
204
|
// DbConnection. Caller owns the lifecycle: `.start()` in boot, `.stop()`
|
|
200
205
|
// in shutdown. Tests drain via `.runOnce()` instead.
|
|
@@ -619,6 +624,7 @@ export function buildServer(options: ServerOptions): KumikoServer {
|
|
|
619
624
|
jwt,
|
|
620
625
|
sseBroker,
|
|
621
626
|
observability,
|
|
627
|
+
dispatcher,
|
|
622
628
|
...(eventDispatcher ? { eventDispatcher } : {}),
|
|
623
629
|
...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
|
|
624
630
|
};
|
|
@@ -23,6 +23,35 @@ describe("defineFeature", () => {
|
|
|
23
23
|
expect(feature.name).toBe("test");
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
test("r.describe() flows into the definition, trimmed", () => {
|
|
27
|
+
const feature = defineFeature("test", (r) => {
|
|
28
|
+
r.describe(" Stores per-tenant widgets. ");
|
|
29
|
+
});
|
|
30
|
+
expect(feature.description).toBe("Stores per-tenant widgets.");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("description is absent when r.describe() is not called", () => {
|
|
34
|
+
const feature = defineFeature("test", () => {});
|
|
35
|
+
expect("description" in feature).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("r.describe() throws when called twice", () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
defineFeature("test", (r) => {
|
|
41
|
+
r.describe("first");
|
|
42
|
+
r.describe("second");
|
|
43
|
+
}),
|
|
44
|
+
).toThrow(/r\.describe\(\) called twice/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("r.describe() throws on empty or whitespace-only text", () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
defineFeature("test", (r) => {
|
|
50
|
+
r.describe(" ");
|
|
51
|
+
}),
|
|
52
|
+
).toThrow(/non-empty string/);
|
|
53
|
+
});
|
|
54
|
+
|
|
26
55
|
test("collects entities", () => {
|
|
27
56
|
const feature = defineFeature("test", (r) => {
|
|
28
57
|
r.entity(
|
|
@@ -153,6 +153,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
153
153
|
|
|
154
154
|
let isSystemScoped = false;
|
|
155
155
|
let toggleableDefault: boolean | undefined;
|
|
156
|
+
let description: string | undefined;
|
|
156
157
|
// Visual-Tree-Slots — at-most-one per feature, only-once-guard im
|
|
157
158
|
// registrar (siehe r.treeActions / r.tree). Undefined wenn das Feature
|
|
158
159
|
// keinen Visual-Tree-Beitrag liefert (Zero-Whitelist-Filter).
|
|
@@ -182,6 +183,18 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
182
183
|
isSystemScoped = true;
|
|
183
184
|
},
|
|
184
185
|
|
|
186
|
+
describe(text: string): void {
|
|
187
|
+
if (description !== undefined) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[Feature ${name}] r.describe() called twice — a feature's description is declared once`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (typeof text !== "string" || text.trim().length === 0) {
|
|
193
|
+
throw new Error(`[Feature ${name}] r.describe(): text must be a non-empty string`);
|
|
194
|
+
}
|
|
195
|
+
description = text.trim();
|
|
196
|
+
},
|
|
197
|
+
|
|
185
198
|
requires: (() => {
|
|
186
199
|
const fn = (...featureNames: string[]) => {
|
|
187
200
|
requires.push(...featureNames);
|
|
@@ -888,6 +901,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
888
901
|
|
|
889
902
|
return {
|
|
890
903
|
name,
|
|
904
|
+
...(description !== undefined && { description }),
|
|
891
905
|
systemScope: isSystemScoped,
|
|
892
906
|
exports,
|
|
893
907
|
requires,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CallExpression, SourceFile } from "ts-morph";
|
|
2
2
|
import type {
|
|
3
|
+
DescribePattern,
|
|
3
4
|
OptionalRequiresPattern,
|
|
4
5
|
ReadsConfigPattern,
|
|
5
6
|
RequiresPattern,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
fail,
|
|
13
14
|
ok,
|
|
14
15
|
readBooleanProperty,
|
|
16
|
+
readStringLiteralArgs,
|
|
15
17
|
readVarargsOrArrayProp,
|
|
16
18
|
} from "./shared";
|
|
17
19
|
|
|
@@ -82,6 +84,26 @@ export function extractSystemScope(
|
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
export function extractDescribe(
|
|
88
|
+
call: CallExpression,
|
|
89
|
+
sourceFile: SourceFile,
|
|
90
|
+
): ExtractOutput<DescribePattern> {
|
|
91
|
+
const args = readStringLiteralArgs(call);
|
|
92
|
+
const text = args?.[0];
|
|
93
|
+
if (text === undefined || args?.length !== 1) {
|
|
94
|
+
return fail(
|
|
95
|
+
"describe",
|
|
96
|
+
sourceLocationFromNode(call, sourceFile),
|
|
97
|
+
"expected a single string literal",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return ok({
|
|
101
|
+
kind: "describe",
|
|
102
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
103
|
+
text,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
export function extractToggleable(
|
|
86
108
|
call: CallExpression,
|
|
87
109
|
sourceFile: SourceFile,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
extractClaimKey,
|
|
31
31
|
extractConfig,
|
|
32
32
|
extractDefineEvent,
|
|
33
|
+
extractDescribe,
|
|
33
34
|
extractEntity,
|
|
34
35
|
extractEntityHook,
|
|
35
36
|
extractEnvSchema,
|
|
@@ -297,6 +298,8 @@ function dispatchExtractor(
|
|
|
297
298
|
return extractSystemScope(call, sourceFile);
|
|
298
299
|
case "toggleable":
|
|
299
300
|
return extractToggleable(call, sourceFile);
|
|
301
|
+
case "describe":
|
|
302
|
+
return extractDescribe(call, sourceFile);
|
|
300
303
|
// Round 2 — object-literal-based static patterns
|
|
301
304
|
case "entity":
|
|
302
305
|
return extractEntity(call, sourceFile);
|
|
@@ -79,6 +79,7 @@ export type PatternId =
|
|
|
79
79
|
| { readonly kind: "readsConfig" }
|
|
80
80
|
| { readonly kind: "systemScope" }
|
|
81
81
|
| { readonly kind: "toggleable" }
|
|
82
|
+
| { readonly kind: "describe" }
|
|
82
83
|
| { readonly kind: "config" }
|
|
83
84
|
| { readonly kind: "translations" }
|
|
84
85
|
| { readonly kind: "authClaims" }
|
|
@@ -270,6 +271,7 @@ export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
|
|
|
270
271
|
"readsConfig",
|
|
271
272
|
"systemScope",
|
|
272
273
|
"toggleable",
|
|
274
|
+
"describe",
|
|
273
275
|
"config",
|
|
274
276
|
"translations",
|
|
275
277
|
"authClaims",
|
|
@@ -326,6 +328,7 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
326
328
|
case "readsConfig":
|
|
327
329
|
case "systemScope":
|
|
328
330
|
case "toggleable":
|
|
331
|
+
case "describe":
|
|
329
332
|
case "config":
|
|
330
333
|
case "translations":
|
|
331
334
|
case "authClaims":
|
|
@@ -197,6 +197,7 @@ export type AddRequiresArgs = { readonly features: readonly string[] };
|
|
|
197
197
|
export type AddOptionalRequiresArgs = { readonly features: readonly string[] };
|
|
198
198
|
export type AddReadsConfigArgs = { readonly keys: readonly string[] };
|
|
199
199
|
export type AddToggleableArgs = { readonly default: boolean };
|
|
200
|
+
export type AddDescribeArgs = { readonly text: string };
|
|
200
201
|
export type AddNavArgs = { readonly definition: NavDefinition };
|
|
201
202
|
export type AddWorkspaceArgs = { readonly definition: WorkspaceDefinition };
|
|
202
203
|
export type AddConfigArgs = {
|
|
@@ -223,6 +224,7 @@ export type FeaturePatcher = {
|
|
|
223
224
|
readonly addReadsConfig: (args: AddReadsConfigArgs) => void;
|
|
224
225
|
readonly addSystemScope: () => void;
|
|
225
226
|
readonly addToggleable: (args: AddToggleableArgs) => void;
|
|
227
|
+
readonly addDescribe: (args: AddDescribeArgs) => void;
|
|
226
228
|
readonly addEntity: (args: AddEntityArgs) => void;
|
|
227
229
|
readonly addRelation: (args: AddRelationArgs) => void;
|
|
228
230
|
readonly addNav: (args: AddNavArgs) => void;
|
|
@@ -300,6 +302,9 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
300
302
|
addToggleable({ default: defaultOn }) {
|
|
301
303
|
add({ kind: "toggleable", source: SYNTHETIC_LOC, default: defaultOn });
|
|
302
304
|
},
|
|
305
|
+
addDescribe({ text }) {
|
|
306
|
+
add({ kind: "describe", source: SYNTHETIC_LOC, text });
|
|
307
|
+
},
|
|
303
308
|
addEntity({ name, definition }) {
|
|
304
309
|
add({
|
|
305
310
|
kind: "entity",
|
|
@@ -138,6 +138,12 @@ export type ToggleablePattern = {
|
|
|
138
138
|
readonly default: boolean;
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
export type DescribePattern = {
|
|
142
|
+
readonly kind: "describe";
|
|
143
|
+
readonly source: SourceLocation;
|
|
144
|
+
readonly text: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
141
147
|
export type MetricPattern = {
|
|
142
148
|
readonly kind: "metric";
|
|
143
149
|
readonly source: SourceLocation;
|
|
@@ -419,6 +425,7 @@ export type FeaturePattern =
|
|
|
419
425
|
| OptionalRequiresPattern
|
|
420
426
|
| SystemScopePattern
|
|
421
427
|
| ToggleablePattern
|
|
428
|
+
| DescribePattern
|
|
422
429
|
| MetricPattern
|
|
423
430
|
| SecretPattern
|
|
424
431
|
| ClaimKeyPattern
|
|
@@ -476,6 +483,7 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
476
483
|
case "optionalRequires":
|
|
477
484
|
case "systemScope":
|
|
478
485
|
case "toggleable":
|
|
486
|
+
case "describe":
|
|
479
487
|
case "metric":
|
|
480
488
|
case "secret":
|
|
481
489
|
case "claimKey":
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
ClaimKeyPattern,
|
|
22
22
|
ConfigPattern,
|
|
23
23
|
DefineEventPattern,
|
|
24
|
+
DescribePattern,
|
|
24
25
|
EntityHookPattern,
|
|
25
26
|
EntityPattern,
|
|
26
27
|
EnvSchemaPattern,
|
|
@@ -77,6 +78,8 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
77
78
|
return renderSystemScope(pattern);
|
|
78
79
|
case "toggleable":
|
|
79
80
|
return renderToggleable(pattern);
|
|
81
|
+
case "describe":
|
|
82
|
+
return renderDescribe(pattern);
|
|
80
83
|
case "entity":
|
|
81
84
|
return renderEntity(pattern);
|
|
82
85
|
case "relation":
|
|
@@ -232,6 +235,10 @@ function renderToggleable(p: ToggleablePattern): string {
|
|
|
232
235
|
return `r.toggleable({ default: ${p.default} });`;
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
function renderDescribe(p: DescribePattern): string {
|
|
239
|
+
return `r.describe(${JSON.stringify(p.text)});`;
|
|
240
|
+
}
|
|
241
|
+
|
|
235
242
|
function renderEntity(p: EntityPattern): string {
|
|
236
243
|
// Inline `name` into the definition object — canonical Object-Form
|
|
237
244
|
// is a single arg with name-as-property.
|
|
@@ -30,6 +30,7 @@ const ALL_KINDS: FeaturePatternKind[] = [
|
|
|
30
30
|
"readsConfig",
|
|
31
31
|
"systemScope",
|
|
32
32
|
"toggleable",
|
|
33
|
+
"describe",
|
|
33
34
|
"entity",
|
|
34
35
|
"relation",
|
|
35
36
|
"nav",
|
|
@@ -197,6 +198,8 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
197
198
|
return { kind, source: PLACEHOLDER_LOC };
|
|
198
199
|
case "toggleable":
|
|
199
200
|
return { kind, source: PLACEHOLDER_LOC, default: false };
|
|
201
|
+
case "describe":
|
|
202
|
+
return { kind, source: PLACEHOLDER_LOC, text: "x" };
|
|
200
203
|
case "entity":
|
|
201
204
|
return { kind, source: PLACEHOLDER_LOC, entityName: "x", definition: { fields: {} } };
|
|
202
205
|
case "relation":
|
|
@@ -176,6 +176,24 @@ const toggleableSchema: PatternFormSchema = {
|
|
|
176
176
|
],
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
+
const describeSchema: PatternFormSchema = {
|
|
180
|
+
kind: "describe",
|
|
181
|
+
label: { en: "Description", de: "Beschreibung" },
|
|
182
|
+
summary: { en: "One-to-three-sentence docs-lead: what the feature does + when you need it." },
|
|
183
|
+
category: "meta",
|
|
184
|
+
editability: "static",
|
|
185
|
+
singleton: true,
|
|
186
|
+
fields: [
|
|
187
|
+
{
|
|
188
|
+
path: "text",
|
|
189
|
+
label: { en: "Text", de: "Text" },
|
|
190
|
+
input: "textarea",
|
|
191
|
+
required: true,
|
|
192
|
+
placeholder: "Stores per-tenant widgets and exposes CRUD handlers for them.",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
|
|
179
197
|
const entitySchema: PatternFormSchema = {
|
|
180
198
|
kind: "entity",
|
|
181
199
|
label: { en: "Entity", de: "Entität" },
|
|
@@ -1142,6 +1160,7 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
1142
1160
|
readsConfig: readsConfigSchema,
|
|
1143
1161
|
systemScope: systemScopeSchema,
|
|
1144
1162
|
toggleable: toggleableSchema,
|
|
1163
|
+
describe: describeSchema,
|
|
1145
1164
|
entity: entitySchema,
|
|
1146
1165
|
relation: relationSchema,
|
|
1147
1166
|
nav: navSchema,
|
|
@@ -7,11 +7,19 @@ import type { TenantId } from "./types/identifiers";
|
|
|
7
7
|
export const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
8
8
|
export const SYSTEM_ROLE = "system" as const;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// extraRoles: hasAccess kennt keinen System-Bypass — Handler gaten auf
|
|
11
|
+
// explizite Rollen. Caller, die Handler mit z.B. SystemAdmin-Gate erreichen
|
|
12
|
+
// müssen (extraRoutes.dispatchSystemWrite → billing-foundation
|
|
13
|
+
// process-event), geben die Rolle hier zusätzlich mit; createdBy bleibt
|
|
14
|
+
// SYSTEM_USER_ID, der Audit-Trail zeigt weiterhin System.
|
|
15
|
+
export function createSystemUser(
|
|
16
|
+
tenantId: TenantId,
|
|
17
|
+
extraRoles: readonly string[] = [],
|
|
18
|
+
): SessionUser {
|
|
11
19
|
return {
|
|
12
20
|
id: SYSTEM_USER_ID,
|
|
13
21
|
tenantId,
|
|
14
|
-
roles: [SYSTEM_ROLE],
|
|
22
|
+
roles: [SYSTEM_ROLE, ...extraRoles],
|
|
15
23
|
};
|
|
16
24
|
}
|
|
17
25
|
|
|
@@ -170,6 +170,9 @@ export type UnmanagedTableDef = UnmanagedTableEntry & {
|
|
|
170
170
|
|
|
171
171
|
export type FeatureDefinition = {
|
|
172
172
|
readonly name: string;
|
|
173
|
+
// Docs-lead paragraph declared via r.describe(). Flows through the
|
|
174
|
+
// manifest introspection into the generated feature-reference pages.
|
|
175
|
+
readonly description?: string;
|
|
173
176
|
readonly systemScope: boolean;
|
|
174
177
|
// Set from the setup-callback return — typed via `defineFeature<TExports>`.
|
|
175
178
|
// `undefined` for setups that return nothing.
|
|
@@ -335,6 +338,9 @@ export type RequiresApi = ((...featureNames: string[]) => void) & {
|
|
|
335
338
|
|
|
336
339
|
export type FeatureRegistrar<TFeature extends string = string> = {
|
|
337
340
|
systemScope(): void;
|
|
341
|
+
// One-to-three-sentence docs-lead for the feature ("what it does + when
|
|
342
|
+
// you need it"). At most once per feature; must be non-empty.
|
|
343
|
+
describe(text: string): void;
|
|
338
344
|
requires: RequiresApi;
|
|
339
345
|
optionalRequires(...featureNames: string[]): void;
|
|
340
346
|
// Declare the feature as operator-togglable. `default` is the effective
|
package/src/entrypoint/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import type { Lifecycle } from "../lifecycle";
|
|
|
45
45
|
import { createLifecycle } from "../lifecycle";
|
|
46
46
|
import type { ObservabilityOptions, ObservabilityProvider } from "../observability";
|
|
47
47
|
import type { EventDedup, EventDispatcher } from "../pipeline";
|
|
48
|
-
import type { DispatcherOptions } from "../pipeline/dispatcher";
|
|
48
|
+
import type { Dispatcher, DispatcherOptions } from "../pipeline/dispatcher";
|
|
49
49
|
import type { SystemHooks } from "../pipeline/lifecycle-pipeline";
|
|
50
50
|
|
|
51
51
|
// Shared fields across all three modes. A caller that swaps between
|
|
@@ -114,9 +114,12 @@ export type ApiEntrypoint = {
|
|
|
114
114
|
readonly sseBroker: SseBroker;
|
|
115
115
|
readonly lifecycle: Lifecycle;
|
|
116
116
|
readonly observability: ObservabilityProvider;
|
|
117
|
+
// Command-dispatcher behind /api/* — for writes outside the HTTP
|
|
118
|
+
// pipeline (provider-webhook routes, see KumikoServer.dispatcher).
|
|
119
|
+
readonly dispatcher: Dispatcher;
|
|
117
120
|
readonly mode: "api";
|
|
118
|
-
// No-op on API mode — dispatcher isn't built, job-runner doesn't
|
|
119
|
-
// Kept for a uniform call-site so `main.ts` doesn't branch on mode.
|
|
121
|
+
// No-op on API mode — event-dispatcher isn't built, job-runner doesn't
|
|
122
|
+
// exist. Kept for a uniform call-site so `main.ts` doesn't branch on mode.
|
|
120
123
|
start(): Promise<void>;
|
|
121
124
|
stop(): Promise<void>;
|
|
122
125
|
};
|
|
@@ -329,6 +332,7 @@ export function createApiEntrypoint(options: ApiEntrypointOptions): ApiEntrypoin
|
|
|
329
332
|
sseBroker: server.sseBroker,
|
|
330
333
|
lifecycle,
|
|
331
334
|
observability: server.observability,
|
|
335
|
+
dispatcher: server.dispatcher,
|
|
332
336
|
mode: "api",
|
|
333
337
|
async start() {
|
|
334
338
|
// Start the local BullMQ worker when runLocalJobs=true; enqueuer-only
|
|
@@ -424,6 +428,7 @@ export function createAllInOneEntrypoint(options: AllInOneEntrypointOptions): Al
|
|
|
424
428
|
eventDispatcher,
|
|
425
429
|
jobRunner: workerJobRunner,
|
|
426
430
|
observability: server.observability,
|
|
431
|
+
dispatcher: server.dispatcher,
|
|
427
432
|
mode: "all-in-one",
|
|
428
433
|
async start() {
|
|
429
434
|
await eventDispatcher.start();
|
package/src/files/feature.ts
CHANGED
|
@@ -16,6 +16,9 @@ export { fileRefEntity } from "./file-ref-entity";
|
|
|
16
16
|
// file-routes + fileRefsTable; bundled-features/files re-exportiert nur.
|
|
17
17
|
export function createFilesFeature(): FeatureDefinition {
|
|
18
18
|
return defineFeature("files", (r) => {
|
|
19
|
+
r.describe(
|
|
20
|
+
"Exposes the `fileRef` entity and `createFilesFeature` from the framework core so that uploaded files \u2014 tracked in the `file_refs` table by `createFileRoutes` \u2014 participate in cross-feature hooks: `user-data-rights-defaults` automatically includes file blobs in GDPR exports and forget flows, and future tenant-lifecycle cleanup will delete all refs on tenant destroy. This feature does not add upload or download routes; those remain in the server bootstrap via the `options.files` parameter.",
|
|
21
|
+
);
|
|
19
22
|
r.entity("fileRef", fileRefEntity);
|
|
20
23
|
});
|
|
21
24
|
}
|
package/src/stack/test-stack.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { FeatureDefinition, Registry, TenantId } from "../engine/types";
|
|
|
10
10
|
import { createArchivedStreamsTable, createEventsTable } from "../event-store";
|
|
11
11
|
import type { Lifecycle } from "../lifecycle";
|
|
12
12
|
import type { ObservabilityProvider } from "../observability";
|
|
13
|
-
import type { EventDispatcher } from "../pipeline";
|
|
13
|
+
import type { Dispatcher, EventDispatcher } from "../pipeline";
|
|
14
14
|
import { createEntityCache, createEventDedup, createIdempotencyGuard } from "../pipeline";
|
|
15
15
|
import { createInMemorySearchAdapter } from "../search";
|
|
16
16
|
import type { SearchAdapter } from "../search/types";
|
|
@@ -31,6 +31,9 @@ export type TestStack = {
|
|
|
31
31
|
events: EventCollector;
|
|
32
32
|
http: RequestHelper;
|
|
33
33
|
observability: ObservabilityProvider;
|
|
34
|
+
// Command-dispatcher behind the HTTP routes — for direct system-writes
|
|
35
|
+
// in tests and dev-server extraRoutes (provider-webhook wiring).
|
|
36
|
+
dispatcher: Dispatcher;
|
|
34
37
|
// Present whenever a system consumer (SSE, Search) or
|
|
35
38
|
// r.multiStreamProjection is wired. Tests drain it via runOnce() for
|
|
36
39
|
// deterministic assertion — no timer-induced flakiness.
|
|
@@ -355,6 +358,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
355
358
|
events,
|
|
356
359
|
http,
|
|
357
360
|
observability: server.observability,
|
|
361
|
+
dispatcher: server.dispatcher,
|
|
358
362
|
...(eventDispatcher ? { eventDispatcher } : {}),
|
|
359
363
|
...(server.lifecycle ? { lifecycle: server.lifecycle } : {}),
|
|
360
364
|
cleanup: async () => {
|