@cosmicdrift/kumiko-framework 0.50.0 → 0.52.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 +2 -2
- package/src/engine/__tests__/boot-validator.test.ts +22 -2
- package/src/engine/boot-validator/config-deps.ts +6 -8
- package/src/engine/boot-validator/index.ts +9 -4
- package/src/engine/config-helpers.ts +6 -0
- package/src/engine/feature-manifest.ts +5 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/types/config.ts +30 -1
- package/src/engine/types/index.ts +1 -0
- package/src/jobs/job-runner.ts +4 -0
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +22 -0
- package/src/migrations/index.ts +4 -0
- package/src/migrations/pending-rebuilds.ts +36 -1
- package/src/pipeline/dispatcher.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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>",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"zod": "^4.4.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
184
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.50.0",
|
|
185
185
|
"@types/uuid": "^11.0.0",
|
|
186
186
|
"bun-types": "^1.3.13",
|
|
187
187
|
"pino-pretty": "^13.1.3"
|
|
@@ -1521,6 +1521,26 @@ describe("boot-validator", () => {
|
|
|
1521
1521
|
).toThrow(/Config-Key "shop:config:typo-here" ist in keiner Feature-Registry deklariert/);
|
|
1522
1522
|
});
|
|
1523
1523
|
|
|
1524
|
+
test("camelCase-Quelle (multi-word) → kanonische kebab-QN matched, kein Throw", () => {
|
|
1525
|
+
// Regression: r.config registriert den camelCase-Objekt-Key
|
|
1526
|
+
// (brandingTitle), die echte QN ist aber `shop:config:branding-title`
|
|
1527
|
+
// (define-feature toKebab't beides). Ein configEdit-Screen, der die
|
|
1528
|
+
// kanonische kebab-QN referenziert, darf NICHT als "unbekannt" failen —
|
|
1529
|
+
// ohne qualifyEntityName im Validator stand dort die rohe camelCase-QN.
|
|
1530
|
+
const feature = defineFeature("shop", (r) => {
|
|
1531
|
+
r.config({ keys: { brandingTitle: createTenantConfig("text", { default: "" }) } });
|
|
1532
|
+
r.screen({
|
|
1533
|
+
id: "settings",
|
|
1534
|
+
type: "configEdit",
|
|
1535
|
+
scope: "tenant",
|
|
1536
|
+
configKeys: { title: "shop:config:branding-title" },
|
|
1537
|
+
fields: { title: { type: "text" } } as never,
|
|
1538
|
+
layout: { sections: [{ title: "Basics", fields: ["title"] }] } as never,
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1524
1544
|
test("extension section ohne component → Throw (Parität zu entityEdit)", () => {
|
|
1525
1545
|
// synthesizeConfigEditScreen reicht die layout 1:1 an RenderEdit weiter —
|
|
1526
1546
|
// eine Extension-Section ohne react/native-Marker rendert sonst stumm leer.
|
|
@@ -2124,11 +2144,11 @@ describe("boot-validator — config key backing × scope", () => {
|
|
|
2124
2144
|
expect(() => validateBoot([feature])).toThrow(/backing="secrets".*requires scope="system"/i);
|
|
2125
2145
|
});
|
|
2126
2146
|
|
|
2127
|
-
test("
|
|
2147
|
+
test("allows backing:secrets when system-scoped (dispatch is wired)", () => {
|
|
2128
2148
|
const feature = defineFeature("billing", (r) => {
|
|
2129
2149
|
r.config({ keys: { apiKey: createSystemConfig("text", { backing: "secrets" }) } });
|
|
2130
2150
|
});
|
|
2131
|
-
expect(() => validateBoot([feature])).toThrow(
|
|
2151
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
2132
2152
|
});
|
|
2133
2153
|
|
|
2134
2154
|
test("a config key without secrets backing boots fine", () => {
|
|
@@ -151,14 +151,12 @@ export function validateConfigKeyBacking(feature: FeatureDefinition): void {
|
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
`[Feature ${feature.name}] Config key "${keyName}" declares backing="secrets", but the secrets read/write dispatch is not yet wired (framework#333). Until then remove backing (the value would store as config-encrypted) or gate the feature behind #333.`,
|
|
161
|
-
);
|
|
154
|
+
// system-scoped backing="secrets" is wired end-to-end: reads dispatch
|
|
155
|
+
// through the resolver's secretsReader, writes through config:write:set/
|
|
156
|
+
// :reset into the secrets store (SYSTEM_TENANT_ID), masked in the query
|
|
157
|
+
// handlers. The runtime contract is that the app provides
|
|
158
|
+
// `extraContext.secrets` (+ a MasterKeyProvider) — a backing="secrets"
|
|
159
|
+
// read/write without it throws loud at request time, not silently.
|
|
162
160
|
}
|
|
163
161
|
}
|
|
164
162
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QnTypes, qualifyEntityName } from "../qualified-name";
|
|
1
2
|
import type { ClaimKeyDefinition, FeatureDefinition } from "../types";
|
|
2
3
|
import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext";
|
|
3
4
|
import {
|
|
@@ -58,14 +59,18 @@ export function validateBoot(features: readonly FeatureDefinition[]): void {
|
|
|
58
59
|
|
|
59
60
|
// Collect all config keys across features (for cross-feature reference validation)
|
|
60
61
|
const allConfigKeys = new Set<string>();
|
|
61
|
-
// Qualified config-key set für ConfigEditScreen-Validation.
|
|
62
|
-
//
|
|
63
|
-
//
|
|
62
|
+
// Qualified config-key set für ConfigEditScreen-Validation. MUSS via
|
|
63
|
+
// qualifyEntityName kanonisiert werden (toKebab auf Feature + Key) — exakt
|
|
64
|
+
// wie define-feature/registry den QN bildet. Der rohe f.configKeys-Key ist
|
|
65
|
+
// der camelCase-Objekt-Key (`brandingTitle`), die echte QN aber
|
|
66
|
+
// `…:config:branding-title`; ohne toKebab failt jeder configEdit-Screen mit
|
|
67
|
+
// multi-word Config-Key fälschlich. allConfigKeys oben nutzt das ältere
|
|
68
|
+
// `feature.short`-Format für validateConfigReads.
|
|
64
69
|
const allConfigKeyQns = new Set<string>();
|
|
65
70
|
for (const f of features) {
|
|
66
71
|
for (const key of Object.keys(f.configKeys)) {
|
|
67
72
|
allConfigKeys.add(`${f.name}.${key}`);
|
|
68
|
-
allConfigKeyQns.add(
|
|
73
|
+
allConfigKeyQns.add(qualifyEntityName(f.name, QnTypes.config, key));
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -64,6 +64,11 @@ type ConfigKeyOptions<T extends ConfigKeyType> = {
|
|
|
64
64
|
encrypted?: boolean;
|
|
65
65
|
options?: readonly string[]; // for select type
|
|
66
66
|
bounds?: T extends "number" ? ConfigBounds : never;
|
|
67
|
+
// Regex enforced at write (set.write) — only meaningful for text keys
|
|
68
|
+
// (never for the other type-tags). Use anchored + length-bounded patterns:
|
|
69
|
+
// the value is tenant-supplied, so an unbounded backtracking regex is a
|
|
70
|
+
// ReDoS vector. See ConfigKeyDefinition.pattern.
|
|
71
|
+
pattern?: T extends "text" ? { regex: string; flags?: string } : never;
|
|
67
72
|
computed?: ConfigComputedFn<T>;
|
|
68
73
|
allowPerRequest?: T extends "text" ? never : boolean;
|
|
69
74
|
required?: boolean;
|
|
@@ -100,6 +105,7 @@ function createConfigKey<T extends ConfigKeyType>(
|
|
|
100
105
|
...(opts.encrypted ? { encrypted: true } : {}),
|
|
101
106
|
...(opts.options ? { options: opts.options } : {}),
|
|
102
107
|
bounds: opts.bounds as ConfigBounds | undefined, // @cast-boundary schema-walk
|
|
108
|
+
...(opts.pattern ? { pattern: opts.pattern } : {}),
|
|
103
109
|
computed: opts.computed,
|
|
104
110
|
...(opts.allowPerRequest === true ? { allowPerRequest: true } : {}),
|
|
105
111
|
...(opts.required === true ? { required: true } : {}),
|
|
@@ -19,6 +19,10 @@ export type ManifestConfigKey = {
|
|
|
19
19
|
readonly computed: boolean;
|
|
20
20
|
readonly options: readonly string[] | null;
|
|
21
21
|
readonly bounds: { readonly min?: number; readonly max?: number } | null;
|
|
22
|
+
// Serializable write-time validator for type="text" keys (hex/https/length).
|
|
23
|
+
// Carried into the manifest as ConfigKeyDefinition.pattern's JSDoc promises
|
|
24
|
+
// (feature-manifest, docgen) — was previously dropped by this serializer.
|
|
25
|
+
readonly pattern: { readonly regex: string; readonly flags?: string } | null;
|
|
22
26
|
readonly writeRoles: readonly string[];
|
|
23
27
|
readonly readRoles: readonly string[];
|
|
24
28
|
};
|
|
@@ -96,6 +100,7 @@ export function buildManifestFromRegistry(
|
|
|
96
100
|
computed: def.computed !== undefined,
|
|
97
101
|
options: def.options ?? null,
|
|
98
102
|
bounds: def.bounds ?? null,
|
|
103
|
+
pattern: def.pattern ?? null,
|
|
99
104
|
writeRoles: def.access.write,
|
|
100
105
|
readRoles: def.access.read,
|
|
101
106
|
});
|
package/src/engine/index.ts
CHANGED
|
@@ -73,6 +73,16 @@ export type ConfigComputedFn<T extends ConfigKeyType = ConfigKeyType> = (
|
|
|
73
73
|
// Tenant-Override. Die backing×scope-Matrix erzwingt der boot-validator.
|
|
74
74
|
export type ConfigBacking = "config" | "secrets";
|
|
75
75
|
|
|
76
|
+
// Minimal read surface the config resolver needs to dispatch a
|
|
77
|
+
// backing="secrets" key to the secrets store, without coupling the engine
|
|
78
|
+
// types to the full SecretsContext. The app's `ctx.secrets` (a SecretsContext)
|
|
79
|
+
// is structurally assignable. Threaded per-call (not at resolver construction)
|
|
80
|
+
// because the resolver is framework-auto-created while `ctx.secrets` is
|
|
81
|
+
// app-provided — only the request context sees both.
|
|
82
|
+
export type ConfigSecretsReader = {
|
|
83
|
+
get(tenantId: TenantId, key: string): Promise<{ readonly reveal: () => string } | undefined>;
|
|
84
|
+
};
|
|
85
|
+
|
|
76
86
|
export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
|
|
77
87
|
readonly type: T;
|
|
78
88
|
readonly default?: ConfigValue<T>;
|
|
@@ -81,6 +91,15 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
|
|
|
81
91
|
readonly encrypted?: boolean;
|
|
82
92
|
readonly options?: readonly string[];
|
|
83
93
|
readonly bounds?: ConfigBounds;
|
|
94
|
+
// Per-key string-pattern validation for type="text". The value must match
|
|
95
|
+
// the regex at write time — set.write hard-rejects a mismatch with
|
|
96
|
+
// ValidationError("invalid_format"), same posture as bounds. Stored as a
|
|
97
|
+
// serializable {regex, flags} pair (not a RegExp/predicate) so it survives
|
|
98
|
+
// JSON like `bounds`/`options` (feature-manifest, docgen) and compiles per
|
|
99
|
+
// write via new RegExp. Keep patterns anchored + length-bounded: the value
|
|
100
|
+
// is tenant-supplied (untrusted), an unbounded catastrophic-backtracking
|
|
101
|
+
// regex applied to it would be a ReDoS vector.
|
|
102
|
+
readonly pattern?: { readonly regex: string; readonly flags?: string };
|
|
84
103
|
readonly computed?: ConfigComputedFn<T>;
|
|
85
104
|
// Per-Request opt-in. Default false — resolveConfigOrParam wirft für
|
|
86
105
|
// Keys ohne diese Marke, auch wenn der Caller paramValue übergibt. Das
|
|
@@ -145,6 +164,10 @@ export type ConfigAccessor = {
|
|
|
145
164
|
export type ConfigAccessorFactory = (deps: {
|
|
146
165
|
readonly user: { readonly id: string; readonly tenantId: TenantId };
|
|
147
166
|
readonly db: DbConnection | TenantDb;
|
|
167
|
+
// Present when the app wired `extraContext.secrets`. Lets the internal
|
|
168
|
+
// `ctx.config.get` read a backing="secrets" key transparently from the
|
|
169
|
+
// secrets store; absent → a backing="secrets" read throws loud.
|
|
170
|
+
readonly secrets?: ConfigSecretsReader;
|
|
148
171
|
}) => ConfigAccessor;
|
|
149
172
|
|
|
150
173
|
// Row shape returned by ConfigResolver.getAll — just enough for the
|
|
@@ -218,6 +241,7 @@ export type ConfigResolver = {
|
|
|
218
241
|
tenantId: TenantId,
|
|
219
242
|
userId: string,
|
|
220
243
|
db: DbConnection | TenantDb,
|
|
244
|
+
secretsReader?: ConfigSecretsReader,
|
|
221
245
|
): Promise<string | number | boolean | undefined>;
|
|
222
246
|
|
|
223
247
|
// Same cascade as get() but also reports which layer produced the value.
|
|
@@ -231,6 +255,7 @@ export type ConfigResolver = {
|
|
|
231
255
|
tenantId: TenantId,
|
|
232
256
|
userId: string,
|
|
233
257
|
db: DbConnection | TenantDb,
|
|
258
|
+
secretsReader?: ConfigSecretsReader,
|
|
234
259
|
): Promise<ConfigValueWithSource>;
|
|
235
260
|
|
|
236
261
|
getAll(
|
|
@@ -259,17 +284,21 @@ export type ConfigResolver = {
|
|
|
259
284
|
tenantId: TenantId,
|
|
260
285
|
userId: string,
|
|
261
286
|
db: DbConnection | TenantDb,
|
|
287
|
+
secretsReader?: ConfigSecretsReader,
|
|
262
288
|
): Promise<ConfigCascade>;
|
|
263
289
|
|
|
264
290
|
// Batch variant: resolves cascades for N keys in one DB round-trip.
|
|
265
291
|
// keyDefs must contain definitions for every key in the keys array.
|
|
266
|
-
// Returns a map of qualifiedKey → ConfigCascade.
|
|
292
|
+
// Returns a map of qualifiedKey → ConfigCascade. backing="secrets" keys
|
|
293
|
+
// resolve their system rung from the secrets store via secretsReader
|
|
294
|
+
// (one read each — they are system-only and rare).
|
|
267
295
|
getCascadeBatch(
|
|
268
296
|
keys: readonly string[],
|
|
269
297
|
keyDefs: ReadonlyMap<string, ConfigKeyDefinition>,
|
|
270
298
|
tenantId: TenantId,
|
|
271
299
|
userId: string,
|
|
272
300
|
db: DbConnection | TenantDb,
|
|
301
|
+
secretsReader?: ConfigSecretsReader,
|
|
273
302
|
): Promise<ReadonlyMap<string, ConfigCascade>>;
|
|
274
303
|
};
|
|
275
304
|
|
package/src/jobs/job-runner.ts
CHANGED
|
@@ -299,6 +299,10 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
299
299
|
const triggerName = rawData["_triggerName"] as string | undefined; // @cast-boundary dynamic-key
|
|
300
300
|
const jobContext: AppContext = {
|
|
301
301
|
...context,
|
|
302
|
+
// The runner owns the registry it resolved this job from — expose it so
|
|
303
|
+
// workers can reach projections/jobs without the app author duplicating
|
|
304
|
+
// it into `context` (the JobContext contract guarantees `registry`).
|
|
305
|
+
registry,
|
|
302
306
|
systemUser: createSystemUser(tenantId),
|
|
303
307
|
triggeredBy: triggeredById !== null ? { id: triggeredById, tenantId } : null,
|
|
304
308
|
log: createJobLogger(logs),
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
unsafePushTables,
|
|
31
31
|
} from "../../stack";
|
|
32
32
|
import {
|
|
33
|
+
enqueueProjectionRebuild,
|
|
33
34
|
listPendingRebuilds,
|
|
34
35
|
queueRebuildsFromMarkers,
|
|
35
36
|
runPendingRebuilds,
|
|
@@ -226,3 +227,24 @@ describe("pending-rebuilds queue", () => {
|
|
|
226
227
|
expect(await getCount()).toBe(1);
|
|
227
228
|
});
|
|
228
229
|
});
|
|
230
|
+
|
|
231
|
+
// #362: der framework-Helper. Ohne jobs-Feature (kein jobRunner) rebuildet er
|
|
232
|
+
// synchron inline — das heutige Verhalten, garantiert framework-pur. Der
|
|
233
|
+
// dispatch-Pfad (mit jobs) lebt in jobs/__tests__/projection-rebuild-job.*.
|
|
234
|
+
describe("enqueueProjectionRebuild — inline fallback (no jobs feature)", () => {
|
|
235
|
+
test("without a jobRunner, rebuilds the projection synchronously", async () => {
|
|
236
|
+
await executor.create({ groupId: GROUP, name: "a" }, admin, tdb);
|
|
237
|
+
await executor.create({ groupId: GROUP, name: "b" }, admin, tdb);
|
|
238
|
+
|
|
239
|
+
const outcome = await enqueueProjectionRebuild("pendingtest:projection:pending-counts", {
|
|
240
|
+
db: testDb.db,
|
|
241
|
+
registry,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(outcome.mode).toBe("inline");
|
|
245
|
+
if (outcome.mode === "inline") {
|
|
246
|
+
expect(outcome.result.eventsProcessed).toBe(2);
|
|
247
|
+
}
|
|
248
|
+
expect(await getCount()).toBe(2);
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/migrations/index.ts
CHANGED
|
@@ -10,8 +10,12 @@ export {
|
|
|
10
10
|
// Persistente Pending-Rebuild-Queue (survives Rebuild-Failures + Crashes).
|
|
11
11
|
export {
|
|
12
12
|
createPendingRebuildsTable,
|
|
13
|
+
type EnqueueProjectionRebuildDeps,
|
|
14
|
+
type EnqueueProjectionRebuildResult,
|
|
15
|
+
enqueueProjectionRebuild,
|
|
13
16
|
listPendingRebuilds,
|
|
14
17
|
type PendingRebuildRun,
|
|
18
|
+
PROJECTION_REBUILD_JOB,
|
|
15
19
|
pendingRebuildsTable,
|
|
16
20
|
queueRebuildsFromMarkers,
|
|
17
21
|
type RunPendingRebuildsOptions,
|
|
@@ -17,8 +17,9 @@ import { deleteMany, selectMany, upsertOnConflict } from "../db/query";
|
|
|
17
17
|
import { readRebuildMarker } from "../db/rebuild-marker";
|
|
18
18
|
import { tableExists } from "../db/schema-inspection";
|
|
19
19
|
import type { Registry } from "../engine/types";
|
|
20
|
+
import type { JobRunner } from "../jobs";
|
|
20
21
|
import { createFallbackLogger } from "../logging/utils";
|
|
21
|
-
import { rebuildProjection } from "../pipeline";
|
|
22
|
+
import { type RebuildResult, rebuildProjection } from "../pipeline";
|
|
22
23
|
import { unsafePushTables } from "../stack";
|
|
23
24
|
import { buildProjectionTableIndex } from "./projection-table-index";
|
|
24
25
|
|
|
@@ -163,3 +164,37 @@ export async function runPendingRebuilds(
|
|
|
163
164
|
}
|
|
164
165
|
return { rebuilt, failed, unmapped, unresolvedManaged };
|
|
165
166
|
}
|
|
167
|
+
|
|
168
|
+
// Qualified name of the framework-provided projection-rebuild job. Registered
|
|
169
|
+
// by the `jobs` bundled-feature (createJobsFeature) → available whenever jobs
|
|
170
|
+
// is composed. A jobs-less boot has no jobRunner and rebuilds inline instead.
|
|
171
|
+
export const PROJECTION_REBUILD_JOB = "jobs:job:projection-rebuild";
|
|
172
|
+
|
|
173
|
+
export type EnqueueProjectionRebuildResult =
|
|
174
|
+
| { readonly mode: "dispatched"; readonly bullJobId: string }
|
|
175
|
+
| { readonly mode: "inline"; readonly result: RebuildResult };
|
|
176
|
+
|
|
177
|
+
export type EnqueueProjectionRebuildDeps = {
|
|
178
|
+
readonly db: DbConnection;
|
|
179
|
+
readonly registry: Registry;
|
|
180
|
+
// Present + projection-rebuild job registered (jobs composed) → tracked,
|
|
181
|
+
// retryable job (read_job_runs + read_job_run_logs). Absent → inline rebuild.
|
|
182
|
+
readonly jobRunner?: JobRunner;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Triggert einen Single-Projection-Rebuild. Mit `jobs`-Feature (jobRunner +
|
|
186
|
+
* registriertem Job) als getrackter, retrybarer Job; ohne jobs synchron inline
|
|
187
|
+
* (heutiges Verhalten). Capability-Detektion über `registry.getJob`, NICHT
|
|
188
|
+
* `hasFeature` — deterministisch und ohne Toggle-Runtime-Abhängigkeit. */
|
|
189
|
+
export async function enqueueProjectionRebuild(
|
|
190
|
+
projection: string,
|
|
191
|
+
deps: EnqueueProjectionRebuildDeps,
|
|
192
|
+
): Promise<EnqueueProjectionRebuildResult> {
|
|
193
|
+
const { db, registry, jobRunner } = deps;
|
|
194
|
+
if (jobRunner !== undefined && registry.getJob(PROJECTION_REBUILD_JOB) !== undefined) {
|
|
195
|
+
const bullJobId = await jobRunner.dispatch(PROJECTION_REBUILD_JOB, { projection });
|
|
196
|
+
return { mode: "dispatched", bullJobId };
|
|
197
|
+
}
|
|
198
|
+
const result = await rebuildProjection(projection, { db, registry });
|
|
199
|
+
return { mode: "inline", result };
|
|
200
|
+
}
|
|
@@ -278,7 +278,11 @@ export function createDispatcher(
|
|
|
278
278
|
// Mirror notify: only built when the config feature wired its factory.
|
|
279
279
|
const config =
|
|
280
280
|
context._configAccessorFactory && db
|
|
281
|
-
? context._configAccessorFactory({
|
|
281
|
+
? context._configAccessorFactory({
|
|
282
|
+
user: { id: user.id, tenantId: user.tenantId },
|
|
283
|
+
db,
|
|
284
|
+
secrets: context.secrets,
|
|
285
|
+
})
|
|
282
286
|
: undefined;
|
|
283
287
|
|
|
284
288
|
// Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
|
|
@@ -1382,7 +1386,11 @@ export function createDispatcher(
|
|
|
1382
1386
|
}
|
|
1383
1387
|
const db = createTenantDb(dbSource, user.tenantId, "tenant", context.tracer, context.meter);
|
|
1384
1388
|
const configAccessor = context._configAccessorFactory
|
|
1385
|
-
? context._configAccessorFactory({
|
|
1389
|
+
? context._configAccessorFactory({
|
|
1390
|
+
user: { id: user.id, tenantId: user.tenantId },
|
|
1391
|
+
db,
|
|
1392
|
+
secrets: context.secrets,
|
|
1393
|
+
})
|
|
1386
1394
|
: undefined;
|
|
1387
1395
|
return {
|
|
1388
1396
|
db,
|