@geekmidas/cli 10.0.0-alpha.39 → 10.0.0-alpha.40
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/dist/{OpenApiTsGenerator-CeEI4ll9.cjs → OpenApiTsGenerator-B4Kj_qNU.cjs} +12 -5
- package/dist/{OpenApiTsGenerator-CeEI4ll9.cjs.map → OpenApiTsGenerator-B4Kj_qNU.cjs.map} +1 -1
- package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs → OpenApiTsGenerator-CEh3EKN3.mjs} +12 -5
- package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs.map → OpenApiTsGenerator-CEh3EKN3.mjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/discover-B0f_kuNN.cjs.map +1 -1
- package/dist/discover-BLhf7V_J.mjs.map +1 -1
- package/dist/{index-CLH1DgsO.d.cts → index-B3PeGOQp.d.cts} +2 -2
- package/dist/{index-CLH1DgsO.d.cts.map → index-B3PeGOQp.d.cts.map} +1 -1
- package/dist/{index-CST2jOPH.d.mts → index-BOyaGcCm.d.mts} +2 -2
- package/dist/{index-CST2jOPH.d.mts.map → index-BOyaGcCm.d.mts.map} +1 -1
- package/dist/index.cjs +206 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +206 -172
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-BJR912RE.mjs → migrate-BVI3yqcE.mjs} +2 -2
- package/dist/{migrate-BJR912RE.mjs.map → migrate-BVI3yqcE.mjs.map} +1 -1
- package/dist/{migrate-Bu8POqsR.cjs → migrate-CFOUApUt.cjs} +2 -2
- package/dist/{migrate-Bu8POqsR.cjs.map → migrate-CFOUApUt.cjs.map} +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{ready-BXmLS4VB.mjs → ready-D8UXsP6q.mjs} +2 -2
- package/dist/{ready-BXmLS4VB.mjs.map → ready-D8UXsP6q.mjs.map} +1 -1
- package/dist/{ready-B0WVtkjR.cjs → ready-mcxamf73.cjs} +2 -2
- package/dist/{ready-B0WVtkjR.cjs.map → ready-mcxamf73.cjs.map} +1 -1
- package/dist/reconcile/public.d.cts +1 -1
- package/dist/reconcile/public.d.mts +1 -1
- package/dist/{types-B0EVAF6U.d.mts → types-0R124hrY.d.mts} +1 -2
- package/dist/types-0R124hrY.d.mts.map +1 -0
- package/dist/{types-CHKCUKM0.d.cts → types-DfdYe31u.d.cts} +1 -2
- package/dist/types-DfdYe31u.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/{workspace-DZ1G4zow.cjs → workspace-BIqeeqcR.cjs} +167 -33
- package/dist/workspace-BIqeeqcR.cjs.map +1 -0
- package/dist/{workspace-CrFbItPz.mjs → workspace-DwS1JK5u.mjs} +165 -33
- package/dist/workspace-DwS1JK5u.mjs.map +1 -0
- package/package.json +16 -14
- package/dist/types-B0EVAF6U.d.mts.map +0 -1
- package/dist/types-CHKCUKM0.d.cts.map +0 -1
- package/dist/workspace-CrFbItPz.mjs.map +0 -1
- package/dist/workspace-DZ1G4zow.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -6,10 +6,10 @@ const require_config = require("./config-CJ-PAb7M.cjs");
|
|
|
6
6
|
const require_workspace = require("./workspace-RYSYtHP7.cjs");
|
|
7
7
|
const require_storage = require("./storage-CSGoC3bK.cjs");
|
|
8
8
|
const require_store = require("./store-DQgxT_WR.cjs");
|
|
9
|
-
const require_workspace$1 = require("./workspace-
|
|
10
|
-
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-
|
|
9
|
+
const require_workspace$1 = require("./workspace-BIqeeqcR.cjs");
|
|
10
|
+
const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-B4Kj_qNU.cjs");
|
|
11
11
|
const require_openapi = require("./openapi.cjs");
|
|
12
|
-
const require_ready = require("./ready-
|
|
12
|
+
const require_ready = require("./ready-mcxamf73.cjs");
|
|
13
13
|
const require_emit = require("./emit-BdoPFjZf.cjs");
|
|
14
14
|
let commander = require("commander");
|
|
15
15
|
let node_process = require("node:process");
|
|
@@ -47,7 +47,7 @@ let _geekmidas_constructs_subscribers = require("@geekmidas/constructs/subscribe
|
|
|
47
47
|
let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
|
|
48
48
|
let node_dns_promises = require("node:dns/promises");
|
|
49
49
|
//#region package.json
|
|
50
|
-
var version = "10.0.0-alpha.
|
|
50
|
+
var version = "10.0.0-alpha.40";
|
|
51
51
|
//#endregion
|
|
52
52
|
//#region src/auth/credentials.ts
|
|
53
53
|
/**
|
|
@@ -1214,7 +1214,10 @@ function getProductionConfigFromGkm(config) {
|
|
|
1214
1214
|
const serverConfig = config.providers?.server;
|
|
1215
1215
|
if (typeof serverConfig === "object") return serverConfig.production;
|
|
1216
1216
|
}
|
|
1217
|
+
/** Read by the generated server: no subscriber is subscribed or polled. */
|
|
1218
|
+
const SUBSCRIBERS_ENV = "GKM_SUBSCRIBERS";
|
|
1217
1219
|
async function devCommand(options) {
|
|
1220
|
+
if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
|
|
1218
1221
|
if (options.entry) return entryDevCommand(options);
|
|
1219
1222
|
const defaultEnv = require_workspace$1.loadEnvFiles(".env");
|
|
1220
1223
|
if (defaultEnv.loaded.length > 0) logger$10.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
@@ -1569,9 +1572,9 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1569
1572
|
const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
|
|
1570
1573
|
for (const { id, port } of fakes) logger$10.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1571
1574
|
if (options.migrate || options.seed) {
|
|
1572
|
-
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-
|
|
1575
|
+
const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
|
|
1573
1576
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1574
|
-
} else await Promise.resolve().then(() => require("./migrate-
|
|
1577
|
+
} else await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1575
1578
|
logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1576
1579
|
});
|
|
1577
1580
|
const appUrls = {};
|
|
@@ -2216,7 +2219,7 @@ export async function setupCrons(
|
|
|
2216
2219
|
// runtime role, which can neither create a schema nor use the broker's.
|
|
2217
2220
|
const { url } = envParser
|
|
2218
2221
|
.create((get) => ({
|
|
2219
|
-
url: get('
|
|
2222
|
+
url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
|
|
2220
2223
|
}))
|
|
2221
2224
|
.parse();
|
|
2222
2225
|
|
|
@@ -2225,7 +2228,7 @@ export async function setupCrons(
|
|
|
2225
2228
|
{ crons: prepared.map(({ name }) => name) },
|
|
2226
2229
|
'These crons have nowhere to keep their schedule. On a server they are ' +
|
|
2227
2230
|
'scheduled through the pg-boss events broker ' +
|
|
2228
|
-
'(
|
|
2231
|
+
'(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
|
|
2229
2232
|
(url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
|
|
2230
2233
|
);
|
|
2231
2234
|
return;
|
|
@@ -2338,11 +2341,11 @@ export const handler = adapter.handler;
|
|
|
2338
2341
|
//#endregion
|
|
2339
2342
|
//#region src/generators/QueueGenerator.ts
|
|
2340
2343
|
/**
|
|
2341
|
-
* Generates the runtime for
|
|
2344
|
+
* Generates the runtime for queue consumers (`worker.queue(…)`).
|
|
2342
2345
|
*
|
|
2343
2346
|
* - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2347
|
+
* polls each queue in-process, alongside the Hono server, through the queue's
|
|
2348
|
+
* own connection string — pg-boss by default, SQS on the AWS emulator.
|
|
2346
2349
|
* - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
|
|
2347
2350
|
* by an SQS event-source mapping.
|
|
2348
2351
|
*/
|
|
@@ -2417,64 +2420,52 @@ export async function setupQueues(
|
|
|
2417
2420
|
importsByFile.get(importPath)?.push(key);
|
|
2418
2421
|
}
|
|
2419
2422
|
const content = `/**
|
|
2420
|
-
* Generated queue
|
|
2423
|
+
* Generated queue consumers setup.
|
|
2421
2424
|
*
|
|
2422
|
-
*
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2426
|
-
* <NAME>_PUBLISHER_CONNECTION_STRING (pgboss:// locally) reach it.
|
|
2425
|
+
* Each queue's one consumer polls its own queue, through the queue's own
|
|
2426
|
+
* connection string — the one its producers publish on. SQS cannot push, so
|
|
2427
|
+
* this is a poller on every transport: pg-boss locally by default, the SQS
|
|
2428
|
+
* queue on the AWS emulator.
|
|
2427
2429
|
*
|
|
2428
|
-
*
|
|
2430
|
+
* Deployed on Lambda, a queue's consumer is an SQS event source instead.
|
|
2429
2431
|
*/
|
|
2430
2432
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
2431
2433
|
import type { Logger } from '@geekmidas/logger';
|
|
2432
2434
|
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
2433
|
-
import type { EventConnection
|
|
2435
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
2434
2436
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
2435
2437
|
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
2436
2438
|
|
|
2437
2439
|
const queues = [
|
|
2438
|
-
|
|
2440
|
+
${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${(0, _geekmidas_manifest.provideKey)(construct.id, "publisherConnectionString")}' },`).join("\n")}
|
|
2439
2441
|
];
|
|
2440
2442
|
|
|
2441
|
-
const activeSubscribers: EventSubscriber<any>[] = [];
|
|
2442
|
-
|
|
2443
2443
|
export async function setupQueues(
|
|
2444
2444
|
envParser: EnvironmentParser<any>,
|
|
2445
2445
|
logger: Logger,
|
|
2446
2446
|
): Promise<void> {
|
|
2447
|
-
if (queues.length === 0) {
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
logger.info('Setting up queue workers in polling mode (local development)');
|
|
2452
|
-
|
|
2453
|
-
const config = envParser.create((get) => ({
|
|
2454
|
-
connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
|
|
2455
|
-
})).parse();
|
|
2456
|
-
|
|
2457
2447
|
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
2448
|
+
const connections = new Map<string, EventConnection>();
|
|
2458
2449
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
logger.error({ error }, 'Failed to create event connection for queues');
|
|
2466
|
-
return;
|
|
2467
|
-
}
|
|
2450
|
+
for (const { queue, connectionKey } of queues) {
|
|
2451
|
+
const connectionString = process.env[connectionKey];
|
|
2452
|
+
if (!connectionString) {
|
|
2453
|
+
logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2468
2456
|
|
|
2469
|
-
for (const queue of queues) {
|
|
2470
2457
|
try {
|
|
2458
|
+
let connection = connections.get(connectionString);
|
|
2459
|
+
if (!connection) {
|
|
2460
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
2461
|
+
connections.set(connectionString, connection);
|
|
2462
|
+
}
|
|
2471
2463
|
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
2472
|
-
|
|
2473
2464
|
const services = queue.services.length > 0
|
|
2474
2465
|
? await serviceDiscovery.register(queue.services)
|
|
2475
2466
|
: {};
|
|
2476
2467
|
|
|
2477
|
-
// A queue
|
|
2468
|
+
// A queue's messages carry one type — its own name.
|
|
2478
2469
|
await eventSubscriber.subscribe([queue.name], async (message) => {
|
|
2479
2470
|
try {
|
|
2480
2471
|
const validation = await queue.messageSchema['~standard'].validate(message.payload);
|
|
@@ -2488,30 +2479,23 @@ export async function setupQueues(
|
|
|
2488
2479
|
services: services,
|
|
2489
2480
|
logger: queue.logger,
|
|
2490
2481
|
});
|
|
2491
|
-
|
|
2492
|
-
logger.debug({ queue: queue.name }, 'Successfully processed queue message');
|
|
2493
2482
|
} catch (error) {
|
|
2494
2483
|
logger.error({ error, queue: queue.name }, 'Failed to process queue message');
|
|
2495
|
-
//
|
|
2484
|
+
// Rethrown so the transport keeps the message for a retry.
|
|
2485
|
+
throw error;
|
|
2496
2486
|
}
|
|
2497
2487
|
});
|
|
2498
|
-
|
|
2499
|
-
activeSubscribers.push(eventSubscriber);
|
|
2500
|
-
logger.info({ queue: queue.name }, 'Queue worker started polling');
|
|
2488
|
+
logger.info({ queue: queue.name }, 'Queue consumer started polling');
|
|
2501
2489
|
} catch (error) {
|
|
2502
|
-
logger.error({ error, queue: queue.name }, 'Failed to
|
|
2490
|
+
logger.error({ error, queue: queue.name }, 'Failed to set up queue consumer');
|
|
2503
2491
|
}
|
|
2504
2492
|
}
|
|
2505
2493
|
|
|
2506
2494
|
const shutdown = () => {
|
|
2507
|
-
|
|
2508
|
-
for (const _ of activeSubscribers) {
|
|
2509
|
-
connection.stop();
|
|
2510
|
-
}
|
|
2495
|
+
for (const connection of connections.values()) void connection.close();
|
|
2511
2496
|
};
|
|
2512
|
-
|
|
2513
|
-
process.
|
|
2514
|
-
process.on('SIGINT', shutdown);
|
|
2497
|
+
process.once('SIGTERM', shutdown);
|
|
2498
|
+
process.once('SIGINT', shutdown);
|
|
2515
2499
|
}
|
|
2516
2500
|
`;
|
|
2517
2501
|
await (0, node_fs_promises.writeFile)(queuesPath, content);
|
|
@@ -2584,141 +2568,194 @@ import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
|
2584
2568
|
import type { Logger } from '@geekmidas/logger';
|
|
2585
2569
|
|
|
2586
2570
|
export async function setupSubscribers(
|
|
2571
|
+
_app: unknown,
|
|
2587
2572
|
_envParser: EnvironmentParser<any>,
|
|
2588
2573
|
_logger: Logger,
|
|
2589
|
-
): Promise<void
|
|
2574
|
+
): Promise<(port: number) => Promise<void>> {
|
|
2575
|
+
return async () => {};
|
|
2576
|
+
}
|
|
2590
2577
|
`);
|
|
2591
2578
|
return subscribersPath;
|
|
2592
2579
|
}
|
|
2593
2580
|
const importsByFile = /* @__PURE__ */ new Map();
|
|
2594
2581
|
for (const { path, key } of subscribers) {
|
|
2595
2582
|
const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
|
|
2596
|
-
|
|
2597
|
-
importsByFile.get(importPath)?.push(key);
|
|
2583
|
+
importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
|
|
2598
2584
|
}
|
|
2599
2585
|
const content = `/**
|
|
2600
|
-
* Generated subscribers setup
|
|
2586
|
+
* Generated subscribers setup.
|
|
2601
2587
|
*
|
|
2602
|
-
*
|
|
2603
|
-
*
|
|
2588
|
+
* Each subscriber is run against the topic it is bound to, through that
|
|
2589
|
+
* topic's own connection string:
|
|
2604
2590
|
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
2609
|
-
* -
|
|
2610
|
-
*
|
|
2591
|
+
* - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
|
|
2592
|
+
* server, and the route is subscribed to the topic with a filter policy of
|
|
2593
|
+
* the events it named, so SNS fans out to each subscriber. The route runs the
|
|
2594
|
+
* subscriber through the same adaptor a Lambda subscription does.
|
|
2595
|
+
* - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
|
|
2596
|
+
* each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
|
|
2597
|
+
* every subscriber sees every event; replicas of one subscriber share it.
|
|
2611
2598
|
*
|
|
2612
|
-
*
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
* Supported connection strings:
|
|
2617
|
-
* - sqs://region/account-id/queue-name (SQS queue)
|
|
2618
|
-
* - sns://region/account-id/topic-name (SNS topic)
|
|
2619
|
-
* - rabbitmq://host:port/queue-name (RabbitMQ)
|
|
2620
|
-
* - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
|
|
2621
|
-
* - basic://in-memory (In-memory for testing)
|
|
2599
|
+
* GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
|
|
2600
|
+
* GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
|
|
2601
|
+
* emulator it defaults to this server through host.docker.internal.
|
|
2622
2602
|
*/
|
|
2603
|
+
import { connect } from 'node:net';
|
|
2623
2604
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
2624
2605
|
import type { Logger } from '@geekmidas/logger';
|
|
2625
2606
|
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
2626
|
-
import type { EventConnection
|
|
2607
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
2627
2608
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
2628
2609
|
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
2629
2610
|
|
|
2630
2611
|
const subscribers = [
|
|
2631
|
-
|
|
2612
|
+
${subscribers.map(({ key, construct }) => {
|
|
2613
|
+
const connectionKey = construct.topicName ? (0, _geekmidas_manifest.provideKey)((0, _geekmidas_manifest.canonicalId)(construct.topicName), "publisherConnectionString") : void 0;
|
|
2614
|
+
return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
|
|
2615
|
+
}).join("\n")}
|
|
2632
2616
|
];
|
|
2633
2617
|
|
|
2634
|
-
|
|
2618
|
+
/** The route a subscriber's pushes arrive on. */
|
|
2619
|
+
export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
|
|
2635
2620
|
|
|
2621
|
+
/** Resolves once something is accepting connections on the port. */
|
|
2622
|
+
async function listening(port: number): Promise<void> {
|
|
2623
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
2624
|
+
const open = await new Promise<boolean>((resolve) => {
|
|
2625
|
+
const socket = connect(port, '127.0.0.1');
|
|
2626
|
+
socket.once('connect', () => { socket.end(); resolve(true); });
|
|
2627
|
+
socket.once('error', () => resolve(false));
|
|
2628
|
+
});
|
|
2629
|
+
if (open) return;
|
|
2630
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/**
|
|
2635
|
+
* Mount each pushed subscriber's route and start each polled one. Returns what
|
|
2636
|
+
* to run once the server is listening: subscribing a route makes SNS send its
|
|
2637
|
+
* confirmation at once, so the route has to be reachable first.
|
|
2638
|
+
*/
|
|
2636
2639
|
export async function setupSubscribers(
|
|
2640
|
+
app: { post: (path: string, handler: (c: any) => unknown) => unknown },
|
|
2637
2641
|
envParser: EnvironmentParser<any>,
|
|
2638
2642
|
logger: Logger,
|
|
2639
|
-
): Promise<void
|
|
2640
|
-
if (
|
|
2641
|
-
|
|
2643
|
+
): Promise<(port: number) => Promise<void>> {
|
|
2644
|
+
if (process.env.GKM_SUBSCRIBERS === 'off') {
|
|
2645
|
+
logger.info('Subscribers are off (--no-subscribers)');
|
|
2646
|
+
return async () => {};
|
|
2642
2647
|
}
|
|
2643
2648
|
|
|
2644
|
-
logger.info('Setting up subscribers in polling mode (local development)');
|
|
2645
|
-
|
|
2646
|
-
const config = envParser.create((get) => ({
|
|
2647
|
-
connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
|
|
2648
|
-
})).parse();
|
|
2649
|
-
|
|
2650
2649
|
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
2650
|
+
const connections = new Map<string, EventConnection>();
|
|
2651
|
+
const afterListening: ((port: number) => Promise<void>)[] = [];
|
|
2652
|
+
|
|
2653
|
+
for (const { id, subscriber, topic, connectionKey } of subscribers) {
|
|
2654
|
+
const events = subscriber.subscribedEvents || [];
|
|
2655
|
+
if (events.length === 0) {
|
|
2656
|
+
logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
const connectionString = connectionKey ? process.env[connectionKey] : undefined;
|
|
2660
|
+
if (!connectionString) {
|
|
2661
|
+
logger.error(
|
|
2662
|
+
{ subscriber: id, connectionKey },
|
|
2663
|
+
'No connection string for the topic this subscriber is bound to',
|
|
2664
|
+
);
|
|
2665
|
+
continue;
|
|
2666
|
+
}
|
|
2651
2667
|
|
|
2652
|
-
// Create connection once, outside the loop (more efficient)
|
|
2653
|
-
// EventConnectionFactory automatically determines the right connection type
|
|
2654
|
-
let connection: EventConnection;
|
|
2655
|
-
try {
|
|
2656
|
-
connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
|
|
2657
|
-
|
|
2658
|
-
const connectionType = new URL(config.connectionString).protocol.replace(':', '');
|
|
2659
|
-
logger.info({ connectionType }, 'Created shared event connection');
|
|
2660
|
-
} catch (error) {
|
|
2661
|
-
logger.error({ error }, 'Failed to create event connection');
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
for (const subscriber of subscribers) {
|
|
2666
2668
|
try {
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
+
if (connectionString.startsWith('sns:')) {
|
|
2670
|
+
const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
|
|
2671
|
+
const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/aws');
|
|
2672
|
+
const address = snsUrl.parse(connectionString);
|
|
2673
|
+
const route = SUBSCRIBER_ROUTE(id);
|
|
2674
|
+
const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
|
|
2675
|
+
topicArn: address.topicArn,
|
|
2676
|
+
// An emulator signs nothing. Decided by the address this server was
|
|
2677
|
+
// configured with, never by anything a request says about itself.
|
|
2678
|
+
verify: !address.endpoint,
|
|
2679
|
+
...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
|
|
2680
|
+
});
|
|
2681
|
+
|
|
2682
|
+
app.post(route, async (c: any) => {
|
|
2683
|
+
const body = JSON.parse(await c.req.text());
|
|
2684
|
+
const response = await adaptor.handle(body);
|
|
2685
|
+
return c.json(response.body, response.status);
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
afterListening.push(async (port) => {
|
|
2689
|
+
const base =
|
|
2690
|
+
process.env.GKM_SUBSCRIBER_PUSH_URL ??
|
|
2691
|
+
(address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
|
|
2692
|
+
if (!base) {
|
|
2693
|
+
logger.error(
|
|
2694
|
+
{ subscriber: id },
|
|
2695
|
+
'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
|
|
2696
|
+
);
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
2700
|
+
await subscribeHttpEndpoint(connection, {
|
|
2701
|
+
endpoint: new URL(route, base).toString(),
|
|
2702
|
+
events,
|
|
2703
|
+
});
|
|
2704
|
+
logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
|
|
2705
|
+
});
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2669
2708
|
|
|
2670
|
-
|
|
2709
|
+
let connection = connections.get(connectionString);
|
|
2710
|
+
if (!connection) {
|
|
2711
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
2712
|
+
connections.set(connectionString, connection);
|
|
2713
|
+
}
|
|
2714
|
+
// Named, so a broker with one address for everything (pg-boss) gives
|
|
2715
|
+
// this subscriber a queue of its own: fan-out, not competition.
|
|
2716
|
+
const eventSubscriber = await Subscriber.fromConnection(connection, {
|
|
2717
|
+
topic,
|
|
2718
|
+
subscription: id,
|
|
2719
|
+
});
|
|
2671
2720
|
const services = subscriber.services.length > 0
|
|
2672
2721
|
? await serviceDiscovery.register(subscriber.services)
|
|
2673
2722
|
: {};
|
|
2674
2723
|
|
|
2675
|
-
|
|
2676
|
-
const subscribedEvents = subscriber.subscribedEvents || [];
|
|
2677
|
-
|
|
2678
|
-
if (subscribedEvents.length === 0) {
|
|
2679
|
-
logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
|
|
2680
|
-
continue;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
await eventSubscriber.subscribe(subscribedEvents, async (event) => {
|
|
2724
|
+
await eventSubscriber.subscribe(events, async (event) => {
|
|
2684
2725
|
try {
|
|
2685
|
-
// Process single event (batch of 1)
|
|
2686
2726
|
await subscriber.handler({
|
|
2687
2727
|
events: [event],
|
|
2688
2728
|
services: services as any,
|
|
2689
2729
|
logger: subscriber.logger,
|
|
2690
2730
|
});
|
|
2691
|
-
|
|
2692
|
-
logger.debug({ eventType: event.type }, 'Successfully processed event');
|
|
2693
2731
|
} catch (error) {
|
|
2694
|
-
logger.error({ error, event }, 'Failed to process event');
|
|
2695
|
-
//
|
|
2732
|
+
logger.error({ error, event, subscriber: id }, 'Failed to process event');
|
|
2733
|
+
// Rethrown so the transport keeps the event for a retry — as a queue
|
|
2734
|
+
// consumer's failure does, and as a pushed subscriber's 500 does.
|
|
2735
|
+
throw error;
|
|
2696
2736
|
}
|
|
2697
2737
|
});
|
|
2698
|
-
|
|
2699
|
-
activeSubscribers.push(eventSubscriber);
|
|
2700
|
-
|
|
2701
|
-
logger.info(
|
|
2702
|
-
{
|
|
2703
|
-
events: subscribedEvents,
|
|
2704
|
-
},
|
|
2705
|
-
'Subscriber started polling'
|
|
2706
|
-
);
|
|
2738
|
+
logger.info({ subscriber: id, events }, 'Subscriber started polling');
|
|
2707
2739
|
} catch (error) {
|
|
2708
|
-
logger.error({ error, subscriber:
|
|
2740
|
+
logger.error({ error, subscriber: id }, 'Failed to set up subscriber');
|
|
2709
2741
|
}
|
|
2710
2742
|
}
|
|
2711
2743
|
|
|
2712
|
-
// Setup graceful shutdown
|
|
2713
2744
|
const shutdown = () => {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2745
|
+
for (const connection of connections.values()) void connection.close();
|
|
2746
|
+
};
|
|
2747
|
+
process.once('SIGTERM', shutdown);
|
|
2748
|
+
process.once('SIGINT', shutdown);
|
|
2749
|
+
|
|
2750
|
+
return async (port) => {
|
|
2751
|
+
if (afterListening.length === 0) return;
|
|
2752
|
+
await listening(port);
|
|
2753
|
+
for (const subscribe of afterListening) {
|
|
2754
|
+
await subscribe(port).catch((error) => {
|
|
2755
|
+
logger.error({ error }, 'Failed to subscribe for push');
|
|
2756
|
+
});
|
|
2717
2757
|
}
|
|
2718
2758
|
};
|
|
2719
|
-
|
|
2720
|
-
process.on('SIGTERM', shutdown);
|
|
2721
|
-
process.on('SIGINT', shutdown);
|
|
2722
2759
|
}
|
|
2723
2760
|
`;
|
|
2724
2761
|
await (0, node_fs_promises.writeFile)(subscribersPath, content);
|
|
@@ -8654,11 +8691,7 @@ function generateConnectionUrls(services) {
|
|
|
8654
8691
|
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
8655
8692
|
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
8656
8693
|
if (services.minio) urls.STORAGE_ENDPOINT = generateMinioEndpoint(services.minio);
|
|
8657
|
-
if (services.pgboss)
|
|
8658
|
-
const pgbossUrl = generatePgBossUrl(services.pgboss);
|
|
8659
|
-
urls.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;
|
|
8660
|
-
urls.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;
|
|
8661
|
-
}
|
|
8694
|
+
if (services.pgboss) urls.EVENT_PUBLISHER_CONNECTION_STRING = generatePgBossUrl(services.pgboss);
|
|
8662
8695
|
if (services.mailpit) {
|
|
8663
8696
|
urls.SMTP_HOST = services.mailpit.host;
|
|
8664
8697
|
urls.SMTP_PORT = String(services.mailpit.port);
|
|
@@ -9555,26 +9588,26 @@ function loadPackageJson() {
|
|
|
9555
9588
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
9556
9589
|
*/
|
|
9557
9590
|
const GEEKMIDAS_VERSIONS = {
|
|
9558
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
9559
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
9560
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
9561
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
9562
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
9563
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
9564
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
9565
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
9566
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
9567
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
9568
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
9569
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
9570
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
9571
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
9572
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
9573
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
9574
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
9575
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
9576
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
9577
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
9591
|
+
"@geekmidas/audit": "~10.0.0-alpha.40",
|
|
9592
|
+
"@geekmidas/auth": "~10.0.0-alpha.40",
|
|
9593
|
+
"@geekmidas/cache": "~10.0.0-alpha.40",
|
|
9594
|
+
"@geekmidas/client": "~10.0.0-alpha.40",
|
|
9595
|
+
"@geekmidas/cloud": "~10.0.0-alpha.40",
|
|
9596
|
+
"@geekmidas/constructs": "~10.0.0-alpha.40",
|
|
9597
|
+
"@geekmidas/db": "~10.0.0-alpha.40",
|
|
9598
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.40",
|
|
9599
|
+
"@geekmidas/envkit": "~10.0.0-alpha.40",
|
|
9600
|
+
"@geekmidas/errors": "~10.0.0-alpha.40",
|
|
9601
|
+
"@geekmidas/events": "~10.0.0-alpha.40",
|
|
9602
|
+
"@geekmidas/logger": "~10.0.0-alpha.40",
|
|
9603
|
+
"@geekmidas/manifest": "~10.0.0-alpha.40",
|
|
9604
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.40",
|
|
9605
|
+
"@geekmidas/schema": "~10.0.0-alpha.40",
|
|
9606
|
+
"@geekmidas/services": "~10.0.0-alpha.40",
|
|
9607
|
+
"@geekmidas/storage": "~10.0.0-alpha.40",
|
|
9608
|
+
"@geekmidas/studio": "~10.0.0-alpha.40",
|
|
9609
|
+
"@geekmidas/telescope": "~10.0.0-alpha.40",
|
|
9610
|
+
"@geekmidas/testkit": "~10.0.0-alpha.40",
|
|
9578
9611
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
9579
9612
|
};
|
|
9580
9613
|
//#endregion
|
|
@@ -16323,7 +16356,7 @@ function testDatabases(reconciled) {
|
|
|
16323
16356
|
* starts without the reference data the app needs.
|
|
16324
16357
|
*/
|
|
16325
16358
|
async function migrateTestStage(workspace, env) {
|
|
16326
|
-
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-
|
|
16359
|
+
const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
|
|
16327
16360
|
const { discover } = await Promise.resolve().then(() => require("./discover-B0f_kuNN.cjs")).then((n) => n.discover_exports);
|
|
16328
16361
|
const sources = {};
|
|
16329
16362
|
const manifest = await discover({
|
|
@@ -16723,7 +16756,7 @@ program.command("build").description("Build handlers and the manifest from decla
|
|
|
16723
16756
|
process.exit(1);
|
|
16724
16757
|
}
|
|
16725
16758
|
});
|
|
16726
|
-
program.command("dev").description("Reconcile constructs and start the dev server with automatic reload").option("-p, --port <port>", "Port to run the development server on").option("--entry <file>", "Entry file to run (bypasses gkm config)").option("--watch", "Watch for file changes (default: true with --entry)").option("--no-watch", "Disable file watching").option("--enable-openapi", "Enable OpenAPI documentation for development server", true).option("--migrate", "Apply pending migrations before the apps start").option("--seed", "Migrate, then run the seeds, before the apps start").option("--fake", "Call each external API’s fake (test/fakes/<id>.ts) instead of the provider").action(async (options) => {
|
|
16759
|
+
program.command("dev").description("Reconcile constructs and start the dev server with automatic reload").option("-p, --port <port>", "Port to run the development server on").option("--entry <file>", "Entry file to run (bypasses gkm config)").option("--watch", "Watch for file changes (default: true with --entry)").option("--no-watch", "Disable file watching").option("--enable-openapi", "Enable OpenAPI documentation for development server", true).option("--migrate", "Apply pending migrations before the apps start").option("--seed", "Migrate, then run the seeds, before the apps start").option("--fake", "Call each external API’s fake (test/fakes/<id>.ts) instead of the provider").option("--no-subscribers", "Run no topic subscribers: nothing is subscribed, polled or pushed to").action(async (options) => {
|
|
16727
16760
|
try {
|
|
16728
16761
|
const globalOptions = program.opts();
|
|
16729
16762
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -16735,7 +16768,8 @@ program.command("dev").description("Reconcile constructs and start the dev serve
|
|
|
16735
16768
|
watch: options.watch,
|
|
16736
16769
|
migrate: options.migrate,
|
|
16737
16770
|
seed: options.seed,
|
|
16738
|
-
fake: options.fake
|
|
16771
|
+
fake: options.fake,
|
|
16772
|
+
subscribers: options.subscribers
|
|
16739
16773
|
});
|
|
16740
16774
|
} catch (error) {
|
|
16741
16775
|
console.error(formatError(error));
|
|
@@ -16769,7 +16803,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16769
16803
|
try {
|
|
16770
16804
|
const globalOptions = program.opts();
|
|
16771
16805
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16772
|
-
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
16806
|
+
const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
|
|
16773
16807
|
await migrateCommand({
|
|
16774
16808
|
...options,
|
|
16775
16809
|
...construct ? { construct } : {}
|
|
@@ -16783,7 +16817,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16783
16817
|
try {
|
|
16784
16818
|
const globalOptions = program.opts();
|
|
16785
16819
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16786
|
-
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
16820
|
+
const { seedCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
|
|
16787
16821
|
await seedCommand({
|
|
16788
16822
|
...options,
|
|
16789
16823
|
...construct ? { construct } : {}
|
|
@@ -16797,7 +16831,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16797
16831
|
try {
|
|
16798
16832
|
const globalOptions = program.opts();
|
|
16799
16833
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16800
|
-
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-
|
|
16834
|
+
const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
|
|
16801
16835
|
await migrationCommand({
|
|
16802
16836
|
construct,
|
|
16803
16837
|
...name ? { name } : {}
|
|
@@ -16936,7 +16970,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16936
16970
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16937
16971
|
process.exit(1);
|
|
16938
16972
|
}
|
|
16939
|
-
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-
|
|
16973
|
+
const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-BIqeeqcR.cjs")).then((n) => n.workspace_exports);
|
|
16940
16974
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16941
16975
|
if (!result) {
|
|
16942
16976
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|