@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.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { d as parseModuleConfig, f as appKey, l as loadWorkspaceConfig, n as Not
|
|
|
5
5
|
import { D as assertDeployedStage, E as TEST_STAGE, O as deployedProblems, T as InvalidStages, a as getAppBuildOrder, b as getDeployTargetError, k as stageProblems, m as stripPublicPrefix, o as getAppGkmConfig, p as getPublicEnvPrefix, t as allConstructGlobs, x as isDeployTargetSupported } from "./workspace-DOmIhiau.mjs";
|
|
6
6
|
import { a as withCustomSecret, i as toEmbeddableSecrets, n as maskPassword, t as initStageSecrets } from "./storage-BzaUxmqe.mjs";
|
|
7
7
|
import { a as getKeyPath, i as FileSecretsStore, n as secretsStoreFor } from "./store-DeGATCS-.mjs";
|
|
8
|
-
import { C as findAvailablePort, D as prepareEntryCredentials, E as loadSecretsForApp, S as createEntryWrapper, T as loadEnvFiles, _ as cacheBackendFor, a as reconcileWorkspace, b as storageBackendFor, c as LOCAL_CA_PATH, d as quoteIdentifier, f as pgClient, g as primaryPortKey, h as fakeKey, i as derivedContainers, l as forgetState, m as rootSite, n as backendsOf, p as resourceName, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as eventsBackendFor, w as isPortAvailable, x as createCredentialsPreload, y as providerOf } from "./workspace-
|
|
9
|
-
import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-
|
|
8
|
+
import { C as findAvailablePort, D as prepareEntryCredentials, E as loadSecretsForApp, S as createEntryWrapper, T as loadEnvFiles, _ as cacheBackendFor, a as reconcileWorkspace, b as storageBackendFor, c as LOCAL_CA_PATH, d as quoteIdentifier, f as pgClient, g as primaryPortKey, h as fakeKey, i as derivedContainers, l as forgetState, m as rootSite, n as backendsOf, p as resourceName, r as constructGlobs, s as COMPOSE_PATH, t as FAKE_ENV, u as applyPostgres, v as eventsBackendFor, w as isPortAvailable, x as createCredentialsPreload, y as providerOf } from "./workspace-DwS1JK5u.mjs";
|
|
9
|
+
import { n as EndpointGenerator, r as runtimeFor } from "./OpenApiTsGenerator-CEh3EKN3.mjs";
|
|
10
10
|
import { generateOpenApi, openapiCommand, resolveOpenApiConfig } from "./openapi.mjs";
|
|
11
|
-
import { a as writeTestHarness, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as cacheBackendsIn, s as driversFor, t as TEST_READY_ENV } from "./ready-
|
|
11
|
+
import { a as writeTestHarness, i as TEST_MANIFEST_ENV, n as TEST_READY_FILE, o as cacheBackendsIn, s as driversFor, t as TEST_READY_ENV } from "./ready-D8UXsP6q.mjs";
|
|
12
12
|
import { i as withRoutes, r as withCompute } from "./emit-Bzq7JsMP.mjs";
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { Command } from "commander";
|
|
@@ -41,7 +41,7 @@ import { Subscriber } from "@geekmidas/constructs/subscribers";
|
|
|
41
41
|
import { Topic } from "@geekmidas/constructs/topic";
|
|
42
42
|
import { lookup } from "node:dns/promises";
|
|
43
43
|
//#region package.json
|
|
44
|
-
var version = "10.0.0-alpha.
|
|
44
|
+
var version = "10.0.0-alpha.40";
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region src/auth/credentials.ts
|
|
47
47
|
/**
|
|
@@ -1208,7 +1208,10 @@ function getProductionConfigFromGkm(config) {
|
|
|
1208
1208
|
const serverConfig = config.providers?.server;
|
|
1209
1209
|
if (typeof serverConfig === "object") return serverConfig.production;
|
|
1210
1210
|
}
|
|
1211
|
+
/** Read by the generated server: no subscriber is subscribed or polled. */
|
|
1212
|
+
const SUBSCRIBERS_ENV = "GKM_SUBSCRIBERS";
|
|
1211
1213
|
async function devCommand(options) {
|
|
1214
|
+
if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
|
|
1212
1215
|
if (options.entry) return entryDevCommand(options);
|
|
1213
1216
|
const defaultEnv = loadEnvFiles(".env");
|
|
1214
1217
|
if (defaultEnv.loaded.length > 0) logger$10.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
|
|
@@ -1563,9 +1566,9 @@ async function workspaceDevCommand(workspace, options) {
|
|
|
1563
1566
|
const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
|
|
1564
1567
|
for (const { id, port } of fakes) logger$10.log(`🎭 ${id} fake: http://localhost:${port}`);
|
|
1565
1568
|
if (options.migrate || options.seed) {
|
|
1566
|
-
const { prepareDevDatabases } = await import("./migrate-
|
|
1569
|
+
const { prepareDevDatabases } = await import("./migrate-BVI3yqcE.mjs");
|
|
1567
1570
|
await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
|
|
1568
|
-
} else await import("./migrate-
|
|
1571
|
+
} else await import("./migrate-BVI3yqcE.mjs").then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
|
|
1569
1572
|
logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
|
|
1570
1573
|
});
|
|
1571
1574
|
const appUrls = {};
|
|
@@ -2210,7 +2213,7 @@ export async function setupCrons(
|
|
|
2210
2213
|
// runtime role, which can neither create a schema nor use the broker's.
|
|
2211
2214
|
const { url } = envParser
|
|
2212
2215
|
.create((get) => ({
|
|
2213
|
-
url: get('
|
|
2216
|
+
url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
|
|
2214
2217
|
}))
|
|
2215
2218
|
.parse();
|
|
2216
2219
|
|
|
@@ -2219,7 +2222,7 @@ export async function setupCrons(
|
|
|
2219
2222
|
{ crons: prepared.map(({ name }) => name) },
|
|
2220
2223
|
'These crons have nowhere to keep their schedule. On a server they are ' +
|
|
2221
2224
|
'scheduled through the pg-boss events broker ' +
|
|
2222
|
-
'(
|
|
2225
|
+
'(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
|
|
2223
2226
|
(url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
|
|
2224
2227
|
);
|
|
2225
2228
|
return;
|
|
@@ -2332,11 +2335,11 @@ export const handler = adapter.handler;
|
|
|
2332
2335
|
//#endregion
|
|
2333
2336
|
//#region src/generators/QueueGenerator.ts
|
|
2334
2337
|
/**
|
|
2335
|
-
* Generates the runtime for
|
|
2338
|
+
* Generates the runtime for queue consumers (`worker.queue(…)`).
|
|
2336
2339
|
*
|
|
2337
2340
|
* - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2341
|
+
* polls each queue in-process, alongside the Hono server, through the queue's
|
|
2342
|
+
* own connection string — pg-boss by default, SQS on the AWS emulator.
|
|
2340
2343
|
* - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
|
|
2341
2344
|
* by an SQS event-source mapping.
|
|
2342
2345
|
*/
|
|
@@ -2411,64 +2414,52 @@ export async function setupQueues(
|
|
|
2411
2414
|
importsByFile.get(importPath)?.push(key);
|
|
2412
2415
|
}
|
|
2413
2416
|
const content = `/**
|
|
2414
|
-
* Generated queue
|
|
2417
|
+
* Generated queue consumers setup.
|
|
2415
2418
|
*
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
* <NAME>_PUBLISHER_CONNECTION_STRING (pgboss:// locally) reach it.
|
|
2419
|
+
* Each queue's one consumer polls its own queue, through the queue's own
|
|
2420
|
+
* connection string — the one its producers publish on. SQS cannot push, so
|
|
2421
|
+
* this is a poller on every transport: pg-boss locally by default, the SQS
|
|
2422
|
+
* queue on the AWS emulator.
|
|
2421
2423
|
*
|
|
2422
|
-
*
|
|
2424
|
+
* Deployed on Lambda, a queue's consumer is an SQS event source instead.
|
|
2423
2425
|
*/
|
|
2424
2426
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
2425
2427
|
import type { Logger } from '@geekmidas/logger';
|
|
2426
2428
|
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
2427
|
-
import type { EventConnection
|
|
2429
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
2428
2430
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
2429
2431
|
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
2430
2432
|
|
|
2431
2433
|
const queues = [
|
|
2432
|
-
|
|
2434
|
+
${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${provideKey(construct.id, "publisherConnectionString")}' },`).join("\n")}
|
|
2433
2435
|
];
|
|
2434
2436
|
|
|
2435
|
-
const activeSubscribers: EventSubscriber<any>[] = [];
|
|
2436
|
-
|
|
2437
2437
|
export async function setupQueues(
|
|
2438
2438
|
envParser: EnvironmentParser<any>,
|
|
2439
2439
|
logger: Logger,
|
|
2440
2440
|
): Promise<void> {
|
|
2441
|
-
if (queues.length === 0) {
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
logger.info('Setting up queue workers in polling mode (local development)');
|
|
2446
|
-
|
|
2447
|
-
const config = envParser.create((get) => ({
|
|
2448
|
-
connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
|
|
2449
|
-
})).parse();
|
|
2450
|
-
|
|
2451
2441
|
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
2442
|
+
const connections = new Map<string, EventConnection>();
|
|
2452
2443
|
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
logger.error({ error }, 'Failed to create event connection for queues');
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2444
|
+
for (const { queue, connectionKey } of queues) {
|
|
2445
|
+
const connectionString = process.env[connectionKey];
|
|
2446
|
+
if (!connectionString) {
|
|
2447
|
+
logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2462
2450
|
|
|
2463
|
-
for (const queue of queues) {
|
|
2464
2451
|
try {
|
|
2452
|
+
let connection = connections.get(connectionString);
|
|
2453
|
+
if (!connection) {
|
|
2454
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
2455
|
+
connections.set(connectionString, connection);
|
|
2456
|
+
}
|
|
2465
2457
|
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
2466
|
-
|
|
2467
2458
|
const services = queue.services.length > 0
|
|
2468
2459
|
? await serviceDiscovery.register(queue.services)
|
|
2469
2460
|
: {};
|
|
2470
2461
|
|
|
2471
|
-
// A queue
|
|
2462
|
+
// A queue's messages carry one type — its own name.
|
|
2472
2463
|
await eventSubscriber.subscribe([queue.name], async (message) => {
|
|
2473
2464
|
try {
|
|
2474
2465
|
const validation = await queue.messageSchema['~standard'].validate(message.payload);
|
|
@@ -2482,30 +2473,23 @@ export async function setupQueues(
|
|
|
2482
2473
|
services: services,
|
|
2483
2474
|
logger: queue.logger,
|
|
2484
2475
|
});
|
|
2485
|
-
|
|
2486
|
-
logger.debug({ queue: queue.name }, 'Successfully processed queue message');
|
|
2487
2476
|
} catch (error) {
|
|
2488
2477
|
logger.error({ error, queue: queue.name }, 'Failed to process queue message');
|
|
2489
|
-
//
|
|
2478
|
+
// Rethrown so the transport keeps the message for a retry.
|
|
2479
|
+
throw error;
|
|
2490
2480
|
}
|
|
2491
2481
|
});
|
|
2492
|
-
|
|
2493
|
-
activeSubscribers.push(eventSubscriber);
|
|
2494
|
-
logger.info({ queue: queue.name }, 'Queue worker started polling');
|
|
2482
|
+
logger.info({ queue: queue.name }, 'Queue consumer started polling');
|
|
2495
2483
|
} catch (error) {
|
|
2496
|
-
logger.error({ error, queue: queue.name }, 'Failed to
|
|
2484
|
+
logger.error({ error, queue: queue.name }, 'Failed to set up queue consumer');
|
|
2497
2485
|
}
|
|
2498
2486
|
}
|
|
2499
2487
|
|
|
2500
2488
|
const shutdown = () => {
|
|
2501
|
-
|
|
2502
|
-
for (const _ of activeSubscribers) {
|
|
2503
|
-
connection.stop();
|
|
2504
|
-
}
|
|
2489
|
+
for (const connection of connections.values()) void connection.close();
|
|
2505
2490
|
};
|
|
2506
|
-
|
|
2507
|
-
process.
|
|
2508
|
-
process.on('SIGINT', shutdown);
|
|
2491
|
+
process.once('SIGTERM', shutdown);
|
|
2492
|
+
process.once('SIGINT', shutdown);
|
|
2509
2493
|
}
|
|
2510
2494
|
`;
|
|
2511
2495
|
await writeFile(queuesPath, content);
|
|
@@ -2578,141 +2562,194 @@ import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
|
2578
2562
|
import type { Logger } from '@geekmidas/logger';
|
|
2579
2563
|
|
|
2580
2564
|
export async function setupSubscribers(
|
|
2565
|
+
_app: unknown,
|
|
2581
2566
|
_envParser: EnvironmentParser<any>,
|
|
2582
2567
|
_logger: Logger,
|
|
2583
|
-
): Promise<void
|
|
2568
|
+
): Promise<(port: number) => Promise<void>> {
|
|
2569
|
+
return async () => {};
|
|
2570
|
+
}
|
|
2584
2571
|
`);
|
|
2585
2572
|
return subscribersPath;
|
|
2586
2573
|
}
|
|
2587
2574
|
const importsByFile = /* @__PURE__ */ new Map();
|
|
2588
2575
|
for (const { path, key } of subscribers) {
|
|
2589
2576
|
const importPath = relative(dirname(subscribersPath), path.relative).replace(/\.ts$/, ".js");
|
|
2590
|
-
|
|
2591
|
-
importsByFile.get(importPath)?.push(key);
|
|
2577
|
+
importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
|
|
2592
2578
|
}
|
|
2593
2579
|
const content = `/**
|
|
2594
|
-
* Generated subscribers setup
|
|
2580
|
+
* Generated subscribers setup.
|
|
2595
2581
|
*
|
|
2596
|
-
*
|
|
2597
|
-
*
|
|
2582
|
+
* Each subscriber is run against the topic it is bound to, through that
|
|
2583
|
+
* topic's own connection string:
|
|
2598
2584
|
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
*
|
|
2603
|
-
* -
|
|
2604
|
-
*
|
|
2585
|
+
* - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
|
|
2586
|
+
* server, and the route is subscribed to the topic with a filter policy of
|
|
2587
|
+
* the events it named, so SNS fans out to each subscriber. The route runs the
|
|
2588
|
+
* subscriber through the same adaptor a Lambda subscription does.
|
|
2589
|
+
* - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
|
|
2590
|
+
* each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
|
|
2591
|
+
* every subscriber sees every event; replicas of one subscriber share it.
|
|
2605
2592
|
*
|
|
2606
|
-
*
|
|
2607
|
-
*
|
|
2608
|
-
*
|
|
2609
|
-
*
|
|
2610
|
-
* Supported connection strings:
|
|
2611
|
-
* - sqs://region/account-id/queue-name (SQS queue)
|
|
2612
|
-
* - sns://region/account-id/topic-name (SNS topic)
|
|
2613
|
-
* - rabbitmq://host:port/queue-name (RabbitMQ)
|
|
2614
|
-
* - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
|
|
2615
|
-
* - basic://in-memory (In-memory for testing)
|
|
2593
|
+
* GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
|
|
2594
|
+
* GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
|
|
2595
|
+
* emulator it defaults to this server through host.docker.internal.
|
|
2616
2596
|
*/
|
|
2597
|
+
import { connect } from 'node:net';
|
|
2617
2598
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
2618
2599
|
import type { Logger } from '@geekmidas/logger';
|
|
2619
2600
|
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
2620
|
-
import type { EventConnection
|
|
2601
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
2621
2602
|
import { ServiceDiscovery } from '@geekmidas/services';
|
|
2622
2603
|
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
2623
2604
|
|
|
2624
2605
|
const subscribers = [
|
|
2625
|
-
|
|
2606
|
+
${subscribers.map(({ key, construct }) => {
|
|
2607
|
+
const connectionKey = construct.topicName ? provideKey(canonicalId(construct.topicName), "publisherConnectionString") : void 0;
|
|
2608
|
+
return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
|
|
2609
|
+
}).join("\n")}
|
|
2626
2610
|
];
|
|
2627
2611
|
|
|
2628
|
-
|
|
2612
|
+
/** The route a subscriber's pushes arrive on. */
|
|
2613
|
+
export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
|
|
2629
2614
|
|
|
2615
|
+
/** Resolves once something is accepting connections on the port. */
|
|
2616
|
+
async function listening(port: number): Promise<void> {
|
|
2617
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
2618
|
+
const open = await new Promise<boolean>((resolve) => {
|
|
2619
|
+
const socket = connect(port, '127.0.0.1');
|
|
2620
|
+
socket.once('connect', () => { socket.end(); resolve(true); });
|
|
2621
|
+
socket.once('error', () => resolve(false));
|
|
2622
|
+
});
|
|
2623
|
+
if (open) return;
|
|
2624
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
/**
|
|
2629
|
+
* Mount each pushed subscriber's route and start each polled one. Returns what
|
|
2630
|
+
* to run once the server is listening: subscribing a route makes SNS send its
|
|
2631
|
+
* confirmation at once, so the route has to be reachable first.
|
|
2632
|
+
*/
|
|
2630
2633
|
export async function setupSubscribers(
|
|
2634
|
+
app: { post: (path: string, handler: (c: any) => unknown) => unknown },
|
|
2631
2635
|
envParser: EnvironmentParser<any>,
|
|
2632
2636
|
logger: Logger,
|
|
2633
|
-
): Promise<void
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
2637
|
+
): Promise<(port: number) => Promise<void>> {
|
|
2638
|
+
if (process.env.GKM_SUBSCRIBERS === 'off') {
|
|
2639
|
+
logger.info('Subscribers are off (--no-subscribers)');
|
|
2640
|
+
return async () => {};
|
|
2636
2641
|
}
|
|
2637
2642
|
|
|
2638
|
-
logger.info('Setting up subscribers in polling mode (local development)');
|
|
2639
|
-
|
|
2640
|
-
const config = envParser.create((get) => ({
|
|
2641
|
-
connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
|
|
2642
|
-
})).parse();
|
|
2643
|
-
|
|
2644
2643
|
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
2644
|
+
const connections = new Map<string, EventConnection>();
|
|
2645
|
+
const afterListening: ((port: number) => Promise<void>)[] = [];
|
|
2646
|
+
|
|
2647
|
+
for (const { id, subscriber, topic, connectionKey } of subscribers) {
|
|
2648
|
+
const events = subscriber.subscribedEvents || [];
|
|
2649
|
+
if (events.length === 0) {
|
|
2650
|
+
logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
const connectionString = connectionKey ? process.env[connectionKey] : undefined;
|
|
2654
|
+
if (!connectionString) {
|
|
2655
|
+
logger.error(
|
|
2656
|
+
{ subscriber: id, connectionKey },
|
|
2657
|
+
'No connection string for the topic this subscriber is bound to',
|
|
2658
|
+
);
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2645
2661
|
|
|
2646
|
-
// Create connection once, outside the loop (more efficient)
|
|
2647
|
-
// EventConnectionFactory automatically determines the right connection type
|
|
2648
|
-
let connection: EventConnection;
|
|
2649
|
-
try {
|
|
2650
|
-
connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
|
|
2651
|
-
|
|
2652
|
-
const connectionType = new URL(config.connectionString).protocol.replace(':', '');
|
|
2653
|
-
logger.info({ connectionType }, 'Created shared event connection');
|
|
2654
|
-
} catch (error) {
|
|
2655
|
-
logger.error({ error }, 'Failed to create event connection');
|
|
2656
|
-
return;
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
for (const subscriber of subscribers) {
|
|
2660
2662
|
try {
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
+
if (connectionString.startsWith('sns:')) {
|
|
2664
|
+
const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
|
|
2665
|
+
const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/aws');
|
|
2666
|
+
const address = snsUrl.parse(connectionString);
|
|
2667
|
+
const route = SUBSCRIBER_ROUTE(id);
|
|
2668
|
+
const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
|
|
2669
|
+
topicArn: address.topicArn,
|
|
2670
|
+
// An emulator signs nothing. Decided by the address this server was
|
|
2671
|
+
// configured with, never by anything a request says about itself.
|
|
2672
|
+
verify: !address.endpoint,
|
|
2673
|
+
...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
|
|
2674
|
+
});
|
|
2675
|
+
|
|
2676
|
+
app.post(route, async (c: any) => {
|
|
2677
|
+
const body = JSON.parse(await c.req.text());
|
|
2678
|
+
const response = await adaptor.handle(body);
|
|
2679
|
+
return c.json(response.body, response.status);
|
|
2680
|
+
});
|
|
2681
|
+
|
|
2682
|
+
afterListening.push(async (port) => {
|
|
2683
|
+
const base =
|
|
2684
|
+
process.env.GKM_SUBSCRIBER_PUSH_URL ??
|
|
2685
|
+
(address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
|
|
2686
|
+
if (!base) {
|
|
2687
|
+
logger.error(
|
|
2688
|
+
{ subscriber: id },
|
|
2689
|
+
'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
|
|
2690
|
+
);
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
2694
|
+
await subscribeHttpEndpoint(connection, {
|
|
2695
|
+
endpoint: new URL(route, base).toString(),
|
|
2696
|
+
events,
|
|
2697
|
+
});
|
|
2698
|
+
logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
|
|
2699
|
+
});
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2663
2702
|
|
|
2664
|
-
|
|
2703
|
+
let connection = connections.get(connectionString);
|
|
2704
|
+
if (!connection) {
|
|
2705
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
2706
|
+
connections.set(connectionString, connection);
|
|
2707
|
+
}
|
|
2708
|
+
// Named, so a broker with one address for everything (pg-boss) gives
|
|
2709
|
+
// this subscriber a queue of its own: fan-out, not competition.
|
|
2710
|
+
const eventSubscriber = await Subscriber.fromConnection(connection, {
|
|
2711
|
+
topic,
|
|
2712
|
+
subscription: id,
|
|
2713
|
+
});
|
|
2665
2714
|
const services = subscriber.services.length > 0
|
|
2666
2715
|
? await serviceDiscovery.register(subscriber.services)
|
|
2667
2716
|
: {};
|
|
2668
2717
|
|
|
2669
|
-
|
|
2670
|
-
const subscribedEvents = subscriber.subscribedEvents || [];
|
|
2671
|
-
|
|
2672
|
-
if (subscribedEvents.length === 0) {
|
|
2673
|
-
logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
await eventSubscriber.subscribe(subscribedEvents, async (event) => {
|
|
2718
|
+
await eventSubscriber.subscribe(events, async (event) => {
|
|
2678
2719
|
try {
|
|
2679
|
-
// Process single event (batch of 1)
|
|
2680
2720
|
await subscriber.handler({
|
|
2681
2721
|
events: [event],
|
|
2682
2722
|
services: services as any,
|
|
2683
2723
|
logger: subscriber.logger,
|
|
2684
2724
|
});
|
|
2685
|
-
|
|
2686
|
-
logger.debug({ eventType: event.type }, 'Successfully processed event');
|
|
2687
2725
|
} catch (error) {
|
|
2688
|
-
logger.error({ error, event }, 'Failed to process event');
|
|
2689
|
-
//
|
|
2726
|
+
logger.error({ error, event, subscriber: id }, 'Failed to process event');
|
|
2727
|
+
// Rethrown so the transport keeps the event for a retry — as a queue
|
|
2728
|
+
// consumer's failure does, and as a pushed subscriber's 500 does.
|
|
2729
|
+
throw error;
|
|
2690
2730
|
}
|
|
2691
2731
|
});
|
|
2692
|
-
|
|
2693
|
-
activeSubscribers.push(eventSubscriber);
|
|
2694
|
-
|
|
2695
|
-
logger.info(
|
|
2696
|
-
{
|
|
2697
|
-
events: subscribedEvents,
|
|
2698
|
-
},
|
|
2699
|
-
'Subscriber started polling'
|
|
2700
|
-
);
|
|
2732
|
+
logger.info({ subscriber: id, events }, 'Subscriber started polling');
|
|
2701
2733
|
} catch (error) {
|
|
2702
|
-
logger.error({ error, subscriber:
|
|
2734
|
+
logger.error({ error, subscriber: id }, 'Failed to set up subscriber');
|
|
2703
2735
|
}
|
|
2704
2736
|
}
|
|
2705
2737
|
|
|
2706
|
-
// Setup graceful shutdown
|
|
2707
2738
|
const shutdown = () => {
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2739
|
+
for (const connection of connections.values()) void connection.close();
|
|
2740
|
+
};
|
|
2741
|
+
process.once('SIGTERM', shutdown);
|
|
2742
|
+
process.once('SIGINT', shutdown);
|
|
2743
|
+
|
|
2744
|
+
return async (port) => {
|
|
2745
|
+
if (afterListening.length === 0) return;
|
|
2746
|
+
await listening(port);
|
|
2747
|
+
for (const subscribe of afterListening) {
|
|
2748
|
+
await subscribe(port).catch((error) => {
|
|
2749
|
+
logger.error({ error }, 'Failed to subscribe for push');
|
|
2750
|
+
});
|
|
2711
2751
|
}
|
|
2712
2752
|
};
|
|
2713
|
-
|
|
2714
|
-
process.on('SIGTERM', shutdown);
|
|
2715
|
-
process.on('SIGINT', shutdown);
|
|
2716
2753
|
}
|
|
2717
2754
|
`;
|
|
2718
2755
|
await writeFile(subscribersPath, content);
|
|
@@ -8642,11 +8679,7 @@ function generateConnectionUrls(services) {
|
|
|
8642
8679
|
if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
|
|
8643
8680
|
if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
|
|
8644
8681
|
if (services.minio) urls.STORAGE_ENDPOINT = generateMinioEndpoint(services.minio);
|
|
8645
|
-
if (services.pgboss)
|
|
8646
|
-
const pgbossUrl = generatePgBossUrl(services.pgboss);
|
|
8647
|
-
urls.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;
|
|
8648
|
-
urls.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;
|
|
8649
|
-
}
|
|
8682
|
+
if (services.pgboss) urls.EVENT_PUBLISHER_CONNECTION_STRING = generatePgBossUrl(services.pgboss);
|
|
8650
8683
|
if (services.mailpit) {
|
|
8651
8684
|
urls.SMTP_HOST = services.mailpit.host;
|
|
8652
8685
|
urls.SMTP_PORT = String(services.mailpit.port);
|
|
@@ -9543,26 +9576,26 @@ function loadPackageJson() {
|
|
|
9543
9576
|
* Run: pnpm --filter @geekmidas/cli sync-versions
|
|
9544
9577
|
*/
|
|
9545
9578
|
const GEEKMIDAS_VERSIONS = {
|
|
9546
|
-
"@geekmidas/audit": "~10.0.0-alpha.
|
|
9547
|
-
"@geekmidas/auth": "~10.0.0-alpha.
|
|
9548
|
-
"@geekmidas/cache": "~10.0.0-alpha.
|
|
9549
|
-
"@geekmidas/client": "~10.0.0-alpha.
|
|
9550
|
-
"@geekmidas/cloud": "~10.0.0-alpha.
|
|
9551
|
-
"@geekmidas/constructs": "~10.0.0-alpha.
|
|
9552
|
-
"@geekmidas/db": "~10.0.0-alpha.
|
|
9553
|
-
"@geekmidas/emailkit": "~10.0.0-alpha.
|
|
9554
|
-
"@geekmidas/envkit": "~10.0.0-alpha.
|
|
9555
|
-
"@geekmidas/errors": "~10.0.0-alpha.
|
|
9556
|
-
"@geekmidas/events": "~10.0.0-alpha.
|
|
9557
|
-
"@geekmidas/logger": "~10.0.0-alpha.
|
|
9558
|
-
"@geekmidas/manifest": "~10.0.0-alpha.
|
|
9559
|
-
"@geekmidas/rate-limit": "~10.0.0-alpha.
|
|
9560
|
-
"@geekmidas/schema": "~10.0.0-alpha.
|
|
9561
|
-
"@geekmidas/services": "~10.0.0-alpha.
|
|
9562
|
-
"@geekmidas/storage": "~10.0.0-alpha.
|
|
9563
|
-
"@geekmidas/studio": "~10.0.0-alpha.
|
|
9564
|
-
"@geekmidas/telescope": "~10.0.0-alpha.
|
|
9565
|
-
"@geekmidas/testkit": "~10.0.0-alpha.
|
|
9579
|
+
"@geekmidas/audit": "~10.0.0-alpha.40",
|
|
9580
|
+
"@geekmidas/auth": "~10.0.0-alpha.40",
|
|
9581
|
+
"@geekmidas/cache": "~10.0.0-alpha.40",
|
|
9582
|
+
"@geekmidas/client": "~10.0.0-alpha.40",
|
|
9583
|
+
"@geekmidas/cloud": "~10.0.0-alpha.40",
|
|
9584
|
+
"@geekmidas/constructs": "~10.0.0-alpha.40",
|
|
9585
|
+
"@geekmidas/db": "~10.0.0-alpha.40",
|
|
9586
|
+
"@geekmidas/emailkit": "~10.0.0-alpha.40",
|
|
9587
|
+
"@geekmidas/envkit": "~10.0.0-alpha.40",
|
|
9588
|
+
"@geekmidas/errors": "~10.0.0-alpha.40",
|
|
9589
|
+
"@geekmidas/events": "~10.0.0-alpha.40",
|
|
9590
|
+
"@geekmidas/logger": "~10.0.0-alpha.40",
|
|
9591
|
+
"@geekmidas/manifest": "~10.0.0-alpha.40",
|
|
9592
|
+
"@geekmidas/rate-limit": "~10.0.0-alpha.40",
|
|
9593
|
+
"@geekmidas/schema": "~10.0.0-alpha.40",
|
|
9594
|
+
"@geekmidas/services": "~10.0.0-alpha.40",
|
|
9595
|
+
"@geekmidas/storage": "~10.0.0-alpha.40",
|
|
9596
|
+
"@geekmidas/studio": "~10.0.0-alpha.40",
|
|
9597
|
+
"@geekmidas/telescope": "~10.0.0-alpha.40",
|
|
9598
|
+
"@geekmidas/testkit": "~10.0.0-alpha.40",
|
|
9566
9599
|
"@geekmidas/cli": `~${loadPackageJson().version}`
|
|
9567
9600
|
};
|
|
9568
9601
|
//#endregion
|
|
@@ -16311,7 +16344,7 @@ function testDatabases(reconciled) {
|
|
|
16311
16344
|
* starts without the reference data the app needs.
|
|
16312
16345
|
*/
|
|
16313
16346
|
async function migrateTestStage(workspace, env) {
|
|
16314
|
-
const { migrateAndSeed } = await import("./migrate-
|
|
16347
|
+
const { migrateAndSeed } = await import("./migrate-BVI3yqcE.mjs");
|
|
16315
16348
|
const { discover } = await import("./discover-BLhf7V_J.mjs").then((n) => n.n);
|
|
16316
16349
|
const sources = {};
|
|
16317
16350
|
const manifest = await discover({
|
|
@@ -16711,7 +16744,7 @@ program.command("build").description("Build handlers and the manifest from decla
|
|
|
16711
16744
|
process.exit(1);
|
|
16712
16745
|
}
|
|
16713
16746
|
});
|
|
16714
|
-
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) => {
|
|
16747
|
+
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) => {
|
|
16715
16748
|
try {
|
|
16716
16749
|
const globalOptions = program.opts();
|
|
16717
16750
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
@@ -16723,7 +16756,8 @@ program.command("dev").description("Reconcile constructs and start the dev serve
|
|
|
16723
16756
|
watch: options.watch,
|
|
16724
16757
|
migrate: options.migrate,
|
|
16725
16758
|
seed: options.seed,
|
|
16726
|
-
fake: options.fake
|
|
16759
|
+
fake: options.fake,
|
|
16760
|
+
subscribers: options.subscribers
|
|
16727
16761
|
});
|
|
16728
16762
|
} catch (error) {
|
|
16729
16763
|
console.error(formatError(error));
|
|
@@ -16757,7 +16791,7 @@ program.command("migrate").description("Apply each database construct's migratio
|
|
|
16757
16791
|
try {
|
|
16758
16792
|
const globalOptions = program.opts();
|
|
16759
16793
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16760
|
-
const { migrateCommand } = await import("./migrate-
|
|
16794
|
+
const { migrateCommand } = await import("./migrate-BVI3yqcE.mjs");
|
|
16761
16795
|
await migrateCommand({
|
|
16762
16796
|
...options,
|
|
16763
16797
|
...construct ? { construct } : {}
|
|
@@ -16771,7 +16805,7 @@ program.command("seed").description("Migrate, then run each database construct's
|
|
|
16771
16805
|
try {
|
|
16772
16806
|
const globalOptions = program.opts();
|
|
16773
16807
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16774
|
-
const { seedCommand } = await import("./migrate-
|
|
16808
|
+
const { seedCommand } = await import("./migrate-BVI3yqcE.mjs");
|
|
16775
16809
|
await seedCommand({
|
|
16776
16810
|
...options,
|
|
16777
16811
|
...construct ? { construct } : {}
|
|
@@ -16785,7 +16819,7 @@ program.command("migration").description("Write a construct's next migration: an
|
|
|
16785
16819
|
try {
|
|
16786
16820
|
const globalOptions = program.opts();
|
|
16787
16821
|
if (globalOptions.cwd) process.chdir(globalOptions.cwd);
|
|
16788
|
-
const { migrationCommand } = await import("./migrate-
|
|
16822
|
+
const { migrationCommand } = await import("./migrate-BVI3yqcE.mjs");
|
|
16789
16823
|
await migrationCommand({
|
|
16790
16824
|
construct,
|
|
16791
16825
|
...name ? { name } : {}
|
|
@@ -16924,7 +16958,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
|
|
|
16924
16958
|
console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
16925
16959
|
process.exit(1);
|
|
16926
16960
|
}
|
|
16927
|
-
const { derivedContainers } = await import("./workspace-
|
|
16961
|
+
const { derivedContainers } = await import("./workspace-DwS1JK5u.mjs").then((n) => n.o);
|
|
16928
16962
|
const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
|
|
16929
16963
|
if (!result) {
|
|
16930
16964
|
console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);
|