@cosmicdrift/kumiko-dev-server 0.125.2 → 0.127.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.127.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
53
|
+
"@cosmicdrift/kumiko-bundled-features": "0.127.0",
|
|
54
|
+
"@cosmicdrift/kumiko-framework": "0.127.0",
|
|
55
55
|
"ts-morph": "^28.0.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
3
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
+
import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import { jobRunLoggerCallbacks } from "../job-run-logger";
|
|
6
|
+
|
|
7
|
+
// ponytail: callbacks shape only — no DB I/O; createTestDb needs TEST_DATABASE_URL (CI unit job has none).
|
|
8
|
+
const mockDb = {} as DbConnection;
|
|
9
|
+
|
|
10
|
+
describe("jobRunLoggerCallbacks", () => {
|
|
11
|
+
test("returns undefined when jobs feature is not registered", () => {
|
|
12
|
+
const registry = createRegistry([
|
|
13
|
+
defineFeature("empty", () => ({ handlers: {}, queries: {} })),
|
|
14
|
+
]);
|
|
15
|
+
expect(jobRunLoggerCallbacks(registry, mockDb)).toBeUndefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns logger callbacks when jobs feature is registered", () => {
|
|
19
|
+
const registry = createRegistry([createJobsFeature()]);
|
|
20
|
+
const callbacks = jobRunLoggerCallbacks(registry, mockDb);
|
|
21
|
+
expect(callbacks).toBeDefined();
|
|
22
|
+
expect(typeof callbacks?.onJobStart).toBe("function");
|
|
23
|
+
expect(typeof callbacks?.onJobComplete).toBe("function");
|
|
24
|
+
expect(typeof callbacks?.onJobFailed).toBe("function");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createJobRunLogger } from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import type { JobRunIn } from "@cosmicdrift/kumiko-framework/engine/types";
|
|
5
|
+
import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
|
|
6
|
+
|
|
7
|
+
export function jobRunLoggerCallbacks(
|
|
8
|
+
registry: Registry,
|
|
9
|
+
db: DbConnection,
|
|
10
|
+
): ReturnType<typeof createJobRunLogger> | undefined {
|
|
11
|
+
if (registry.getFeature("jobs") === undefined) return undefined;
|
|
12
|
+
return createJobRunLogger({ db, registry });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Dev-server parity: consume api + worker lanes when jobs are registered. */
|
|
16
|
+
export async function startDevJobRunners(opts: {
|
|
17
|
+
readonly registry: Registry;
|
|
18
|
+
readonly db: DbConnection;
|
|
19
|
+
readonly context: Record<string, unknown>;
|
|
20
|
+
readonly redisUrl: string;
|
|
21
|
+
}): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
|
|
22
|
+
const jobs = [...opts.registry.getAllJobs().values()];
|
|
23
|
+
if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
|
|
24
|
+
return { runners: [], stop: async () => {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const logger = createJobRunLogger({ db: opts.db, registry: opts.registry });
|
|
28
|
+
const runners: JobRunner[] = [];
|
|
29
|
+
const lanes = new Set(
|
|
30
|
+
jobs.map((j) => j.runIn).filter((lane): lane is JobRunIn => lane !== undefined),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
for (const lane of lanes) {
|
|
34
|
+
const jr = createJobRunner({
|
|
35
|
+
registry: opts.registry,
|
|
36
|
+
context: { ...opts.context, db: opts.db },
|
|
37
|
+
redisUrl: opts.redisUrl,
|
|
38
|
+
consumerLane: lane,
|
|
39
|
+
...logger,
|
|
40
|
+
});
|
|
41
|
+
await jr.start();
|
|
42
|
+
runners.push(jr);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
runners,
|
|
47
|
+
stop: async () => {
|
|
48
|
+
for (const runner of runners) await runner.stop();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type TestStackOptions,
|
|
30
30
|
TestUsers,
|
|
31
31
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
32
|
+
import { startDevJobRunners } from "./boot/job-run-logger";
|
|
32
33
|
import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
|
|
33
34
|
import { injectSchema } from "./inject-schema";
|
|
34
35
|
import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
|
|
@@ -707,6 +708,14 @@ export async function createKumikoServer(
|
|
|
707
708
|
await stack.eventDispatcher.start();
|
|
708
709
|
}
|
|
709
710
|
|
|
711
|
+
const redisUrl = `redis://${stack.redis.redis.options.host}:${stack.redis.redis.options.port}/${stack.redis.redis.options.db}`;
|
|
712
|
+
const devJobRunners = await startDevJobRunners({
|
|
713
|
+
registry: stack.registry,
|
|
714
|
+
db: stack.db,
|
|
715
|
+
context: { db: stack.db, registry: stack.registry },
|
|
716
|
+
redisUrl,
|
|
717
|
+
});
|
|
718
|
+
|
|
710
719
|
// Dev user = TestUsers.admin. Demo features are openToAll but the
|
|
711
720
|
// auth-middleware still needs a valid JWT to let the request past.
|
|
712
721
|
// Nicht genutzt wenn `options.auth` gesetzt ist — dann macht der Client
|
|
@@ -977,6 +986,7 @@ export async function createKumikoServer(
|
|
|
977
986
|
if (stack.eventDispatcher) {
|
|
978
987
|
await stack.eventDispatcher.stop();
|
|
979
988
|
}
|
|
989
|
+
await devJobRunners.stop();
|
|
980
990
|
await stack.cleanup();
|
|
981
991
|
};
|
|
982
992
|
|
package/src/run-dev-app.ts
CHANGED
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
type SessionCallbacks,
|
|
36
36
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
37
37
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
38
|
+
import {
|
|
39
|
+
resolveTenantLifecycleGate,
|
|
40
|
+
TENANT_LIFECYCLE_FEATURE,
|
|
41
|
+
} from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
|
|
38
42
|
import type { PatResolver, SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
39
43
|
import { createInMemoryLoginRateLimiter } from "@cosmicdrift/kumiko-framework/api";
|
|
40
44
|
import {
|
|
@@ -42,6 +46,7 @@ import {
|
|
|
42
46
|
configurePiiSubjectKms,
|
|
43
47
|
type KmsAdapter,
|
|
44
48
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
49
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
45
50
|
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
46
51
|
import {
|
|
47
52
|
collectWriteHandlerQns,
|
|
@@ -396,7 +401,9 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
396
401
|
// db (only concrete after setupTestStack). Wired when the feature is mounted;
|
|
397
402
|
// scopes come from the feature's exports (single source with its handlers).
|
|
398
403
|
let patResolver: PatResolver | undefined;
|
|
404
|
+
let lifecycleDb: DbConnection | undefined;
|
|
399
405
|
const patFeature = features.find((f) => f.name === PAT_FEATURE);
|
|
406
|
+
const tenantLifecycleFeature = features.find((f) => f.name === TENANT_LIFECYCLE_FEATURE);
|
|
400
407
|
const patAuthFragment = patFeature
|
|
401
408
|
? {
|
|
402
409
|
patResolver: (rawToken: string) => {
|
|
@@ -412,6 +419,18 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
412
419
|
}
|
|
413
420
|
: {};
|
|
414
421
|
|
|
422
|
+
const tenantLifecycleAuthFragment = tenantLifecycleFeature
|
|
423
|
+
? {
|
|
424
|
+
resolveTenantLifecycleStatus: async (tenantId: string) => {
|
|
425
|
+
if (!lifecycleDb) {
|
|
426
|
+
throw new Error("[runDevApp] tenant-lifecycle gate accessed before onAfterSetup");
|
|
427
|
+
}
|
|
428
|
+
const gate = await resolveTenantLifecycleGate(lifecycleDb, tenantId);
|
|
429
|
+
return gate ? { status: gate.status } : null;
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
: {};
|
|
433
|
+
|
|
415
434
|
const sessionAuthFragment =
|
|
416
435
|
effectiveAuth?.sessions !== undefined
|
|
417
436
|
? {
|
|
@@ -466,6 +485,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
466
485
|
}),
|
|
467
486
|
...sessionAuthFragment,
|
|
468
487
|
...patAuthFragment,
|
|
488
|
+
...tenantLifecycleAuthFragment,
|
|
469
489
|
...(effectiveAuth.passwordReset && {
|
|
470
490
|
passwordReset: {
|
|
471
491
|
requestHandler: AuthHandlers.requestPasswordReset,
|
|
@@ -494,6 +514,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
494
514
|
},
|
|
495
515
|
}),
|
|
496
516
|
onAfterSetup: async (stack) => {
|
|
517
|
+
lifecycleDb = stack.db;
|
|
497
518
|
// Sprint-8a: build tier-resolver BEFORE any seeds so seeds can rely
|
|
498
519
|
// on the resolver being live (e.g. seed that writes a SystemAdmin's
|
|
499
520
|
// tier-assignment can immediately read tier-cuts).
|
package/src/run-prod-app.ts
CHANGED
|
@@ -66,6 +66,10 @@ import {
|
|
|
66
66
|
SESSIONS_FEATURE,
|
|
67
67
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
68
68
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
69
|
+
import {
|
|
70
|
+
resolveTenantLifecycleGate,
|
|
71
|
+
TENANT_LIFECYCLE_FEATURE,
|
|
72
|
+
} from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
|
|
69
73
|
import { createTextContentApi } from "@cosmicdrift/kumiko-bundled-features/text-content";
|
|
70
74
|
import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
71
75
|
import {
|
|
@@ -134,6 +138,7 @@ import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
|
134
138
|
import Redis from "ioredis";
|
|
135
139
|
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
136
140
|
import { type BootCrypto, resolveBootCrypto } from "./boot/boot-crypto";
|
|
141
|
+
import { jobRunLoggerCallbacks } from "./boot/job-run-logger";
|
|
137
142
|
import { ASSETS_DIR } from "./build-prod-bundle";
|
|
138
143
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
139
144
|
import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
|
|
@@ -1036,6 +1041,16 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
1036
1041
|
};
|
|
1037
1042
|
}
|
|
1038
1043
|
|
|
1044
|
+
const tenantLifecycleFeature = features.find((f) => f.name === TENANT_LIFECYCLE_FEATURE);
|
|
1045
|
+
const tenantLifecycleAuthFragment = tenantLifecycleFeature
|
|
1046
|
+
? {
|
|
1047
|
+
resolveTenantLifecycleStatus: async (tenantId: string) => {
|
|
1048
|
+
const gate = await resolveTenantLifecycleGate(db, tenantId);
|
|
1049
|
+
return gate ? { status: gate.status } : null;
|
|
1050
|
+
},
|
|
1051
|
+
}
|
|
1052
|
+
: undefined;
|
|
1053
|
+
|
|
1039
1054
|
const baseEntrypointOptions = {
|
|
1040
1055
|
registry,
|
|
1041
1056
|
context: {
|
|
@@ -1074,6 +1089,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
1074
1089
|
}),
|
|
1075
1090
|
...sessionAuthFragment,
|
|
1076
1091
|
...patAuthFragment,
|
|
1092
|
+
...tenantLifecycleAuthFragment,
|
|
1077
1093
|
...(effectiveAuth.passwordReset && {
|
|
1078
1094
|
passwordReset: {
|
|
1079
1095
|
requestHandler: AuthHandlers.requestPasswordReset,
|
|
@@ -1118,6 +1134,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
1118
1134
|
const runSingleInstance = options.runSingleInstance ?? options.eventDispatcher?.disabled !== true;
|
|
1119
1135
|
const hasJobs = registry.getAllJobs().size > 0;
|
|
1120
1136
|
const queueNamePrefix = options.jobs?.queueNamePrefix;
|
|
1137
|
+
const jobLogger = jobRunLoggerCallbacks(registry, db);
|
|
1121
1138
|
const dispatcherTunables =
|
|
1122
1139
|
options.eventDispatcher?.pollIntervalMs !== undefined
|
|
1123
1140
|
? { pollIntervalMs: options.eventDispatcher.pollIntervalMs }
|
|
@@ -1126,20 +1143,18 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
1126
1143
|
const entrypoint: ApiEntrypoint | AllInOneEntrypoint = runSingleInstance
|
|
1127
1144
|
? createAllInOneEntrypoint({
|
|
1128
1145
|
...baseEntrypointOptions,
|
|
1129
|
-
// Worker-Seite liest die JobsBlock TOP-LEVEL (nicht nested `jobs` wie
|
|
1130
|
-
// die api-Seite); beide Lane-Runner ziehen redisUrl/prefix von hier.
|
|
1131
1146
|
redisUrl,
|
|
1147
|
+
...jobLogger,
|
|
1132
1148
|
...(queueNamePrefix !== undefined && { queueNamePrefix }),
|
|
1133
1149
|
...(!options.eventDispatcher?.disabled && { eventDispatcher: dispatcherTunables }),
|
|
1134
1150
|
})
|
|
1135
1151
|
: createApiEntrypoint({
|
|
1136
1152
|
...baseEntrypointOptions,
|
|
1137
|
-
// API-only: api-Lane-Jobs laufen lokal, ein dezidierter Worker fährt
|
|
1138
|
-
// worker-Lane + MSPs. createApiEntrypoint liest den nested `jobs`-Block.
|
|
1139
1153
|
...(hasJobs && {
|
|
1140
1154
|
jobs: {
|
|
1141
1155
|
redisUrl,
|
|
1142
1156
|
runLocalJobs: true,
|
|
1157
|
+
...jobLogger,
|
|
1143
1158
|
...(queueNamePrefix !== undefined && { queueNamePrefix }),
|
|
1144
1159
|
},
|
|
1145
1160
|
}),
|