@geekmidas/cli 1.10.15 → 1.10.17
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/CHANGELOG.md +12 -0
- package/dist/{bundler-BWsVDer6.mjs → bundler-B4AackW5.mjs} +2 -2
- package/dist/{bundler-BWsVDer6.mjs.map → bundler-B4AackW5.mjs.map} +1 -1
- package/dist/{bundler-Drh5KoN5.cjs → bundler-BhhfkI9T.cjs} +2 -2
- package/dist/{bundler-Drh5KoN5.cjs.map → bundler-BhhfkI9T.cjs.map} +1 -1
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/{fullstack-secrets-D9rjTNyx.cjs → fullstack-secrets-DOHBU4Rp.cjs} +110 -4
- package/dist/fullstack-secrets-DOHBU4Rp.cjs.map +1 -0
- package/dist/{fullstack-secrets-BIFFv4UZ.mjs → fullstack-secrets-x2Kffx7-.mjs} +99 -5
- package/dist/fullstack-secrets-x2Kffx7-.mjs.map +1 -0
- package/dist/{index-UCsZ_Vkw.d.cts → index-BkibYzso.d.cts} +15 -4
- package/dist/index-BkibYzso.d.cts.map +1 -0
- package/dist/{index-gXAGDSGu.d.mts → index-CY-ieuRp.d.mts} +15 -4
- package/dist/index-CY-ieuRp.d.mts.map +1 -0
- package/dist/index.cjs +332 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +332 -62
- package/dist/index.mjs.map +1 -1
- package/dist/openapi-BYxAWwok.cjs.map +1 -1
- package/dist/openapi-DenF-okj.mjs.map +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/{reconcile-DxTEausy.mjs → reconcile-BLh6rswz.mjs} +2 -2
- package/dist/{reconcile-DxTEausy.mjs.map → reconcile-BLh6rswz.mjs.map} +1 -1
- package/dist/{reconcile-LaaJkFlO.cjs → reconcile-Ch7sIcf8.cjs} +2 -2
- package/dist/{reconcile-LaaJkFlO.cjs.map → reconcile-Ch7sIcf8.cjs.map} +1 -1
- package/dist/{storage-Bu44pwPJ.cjs → storage-B1wvztiJ.cjs} +11 -1
- package/dist/{storage-clMAp4sc.mjs.map → storage-B1wvztiJ.cjs.map} +1 -1
- package/dist/{storage-CauTheT9.mjs → storage-Cs4WBsc4.mjs} +1 -1
- package/dist/{storage-DpqzcjQ5.cjs → storage-DOEtT2Hr.cjs} +1 -1
- package/dist/{storage-clMAp4sc.mjs → storage-dbb9RyBl.mjs} +11 -1
- package/dist/{storage-Bu44pwPJ.cjs.map → storage-dbb9RyBl.mjs.map} +1 -1
- package/dist/{sync-BkalF65h.mjs → sync-COnAugP-.mjs} +1 -1
- package/dist/sync-D1Pa30oV.cjs +4 -0
- package/dist/{sync-BeiI5rFC.cjs → sync-DGXXSk2v.cjs} +2 -2
- package/dist/{sync-BeiI5rFC.cjs.map → sync-DGXXSk2v.cjs.map} +1 -1
- package/dist/{sync-CWJ6tL0s.mjs → sync-D_NowTkZ.mjs} +2 -2
- package/dist/{sync-CWJ6tL0s.mjs.map → sync-D_NowTkZ.mjs.map} +1 -1
- package/dist/{types-DiV9Mbvc.d.mts → types-DdHfUbxk.d.cts} +13 -3
- package/dist/types-DdHfUbxk.d.cts.map +1 -0
- package/dist/{types-JvWj5Ckc.d.cts → types-OszPdw9m.d.mts} +13 -3
- package/dist/types-OszPdw9m.d.mts.map +1 -0
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace-4SP3Gx4Y.cjs.map +1 -1
- package/dist/workspace-D4z4A4cq.mjs.map +1 -1
- package/package.json +4 -4
- package/src/dev/__tests__/entry.spec.ts +3 -5
- package/src/dev/__tests__/index.spec.ts +73 -5
- package/src/dev/index.ts +33 -25
- package/src/docker/compose.ts +130 -2
- package/src/init/__tests__/generators.spec.ts +84 -0
- package/src/init/generators/docker.ts +128 -16
- package/src/init/index.ts +26 -1
- package/src/init/templates/index.ts +28 -0
- package/src/init/versions.ts +1 -1
- package/src/secrets/__tests__/generator.spec.ts +183 -0
- package/src/secrets/generator.ts +116 -4
- package/src/secrets/storage.ts +12 -0
- package/src/secrets/types.ts +11 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +86 -0
- package/src/setup/index.ts +64 -1
- package/src/test/__tests__/index.spec.ts +1 -4
- package/src/types.ts +13 -1
- package/src/workspace/types.ts +13 -2
- package/dist/fullstack-secrets-BIFFv4UZ.mjs.map +0 -1
- package/dist/fullstack-secrets-D9rjTNyx.cjs.map +0 -1
- package/dist/index-UCsZ_Vkw.d.cts.map +0 -1
- package/dist/index-gXAGDSGu.d.mts.map +0 -1
- package/dist/sync-Bp8xRcuQ.cjs +0 -4
- package/dist/types-DiV9Mbvc.d.mts.map +0 -1
- package/dist/types-JvWj5Ckc.d.cts.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EventsBackend } from '../../types.js';
|
|
1
2
|
import type {
|
|
2
3
|
GeneratedFile,
|
|
3
4
|
TemplateConfig,
|
|
@@ -64,13 +65,13 @@ export function generateDockerFiles(
|
|
|
64
65
|
if (isFullstack && dbApps?.length) {
|
|
65
66
|
files.push({
|
|
66
67
|
path: 'docker/postgres/init.sh',
|
|
67
|
-
content: generatePostgresInitScript(dbApps),
|
|
68
|
+
content: generatePostgresInitScript(dbApps, options.services?.events),
|
|
68
69
|
});
|
|
69
70
|
|
|
70
71
|
// Generate .env file for docker-compose (contains db passwords)
|
|
71
72
|
files.push({
|
|
72
73
|
path: 'docker/.env',
|
|
73
|
-
content: generateDockerEnv(dbApps),
|
|
74
|
+
content: generateDockerEnv(dbApps, options.services?.events),
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
}
|
|
@@ -183,6 +184,53 @@ export function generateDockerFiles(
|
|
|
183
184
|
volumes.push(' minio_data:');
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// LocalStack for SNS events
|
|
188
|
+
if (options.services?.events === 'sns') {
|
|
189
|
+
services.push(` localstack:
|
|
190
|
+
image: localstack/localstack:latest
|
|
191
|
+
container_name: ${options.name}-localstack
|
|
192
|
+
restart: unless-stopped
|
|
193
|
+
environment:
|
|
194
|
+
SERVICES: sns,sqs
|
|
195
|
+
AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
|
|
196
|
+
AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
|
|
197
|
+
AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
198
|
+
ports:
|
|
199
|
+
- '\${LOCALSTACK_PORT:-4566}:4566'
|
|
200
|
+
volumes:
|
|
201
|
+
- localstack_data:/var/lib/localstack
|
|
202
|
+
healthcheck:
|
|
203
|
+
test: ['CMD', 'curl', '-f', 'http://localhost:4566/_localstack/health']
|
|
204
|
+
interval: 10s
|
|
205
|
+
timeout: 5s
|
|
206
|
+
retries: 5`);
|
|
207
|
+
volumes.push(' localstack_data:');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// RabbitMQ for rabbitmq events (when not already added by worker template)
|
|
211
|
+
if (options.services?.events === 'rabbitmq' && !hasWorker) {
|
|
212
|
+
services.push(` rabbitmq:
|
|
213
|
+
image: rabbitmq:3-management-alpine
|
|
214
|
+
container_name: ${options.name}-rabbitmq
|
|
215
|
+
restart: unless-stopped
|
|
216
|
+
ports:
|
|
217
|
+
- '\${RABBITMQ_HOST_PORT:-5672}:5672'
|
|
218
|
+
- '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
|
|
219
|
+
environment:
|
|
220
|
+
RABBITMQ_DEFAULT_USER: guest
|
|
221
|
+
RABBITMQ_DEFAULT_PASS: guest
|
|
222
|
+
volumes:
|
|
223
|
+
- rabbitmq_data:/var/lib/rabbitmq
|
|
224
|
+
healthcheck:
|
|
225
|
+
test: ['CMD', 'rabbitmq-diagnostics', 'check_running']
|
|
226
|
+
interval: 10s
|
|
227
|
+
timeout: 5s
|
|
228
|
+
retries: 5`);
|
|
229
|
+
if (!volumes.includes(' rabbitmq_data:')) {
|
|
230
|
+
volumes.push(' rabbitmq_data:');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
186
234
|
// Build docker-compose.yml
|
|
187
235
|
let dockerCompose = `# Use "gkm dev" or "gkm test" to start services.
|
|
188
236
|
# Running "docker compose up" directly will not inject secrets or resolve ports.
|
|
@@ -209,12 +257,20 @@ ${volumes.join('\n')}
|
|
|
209
257
|
/**
|
|
210
258
|
* Generate .env file for docker-compose with database passwords
|
|
211
259
|
*/
|
|
212
|
-
function generateDockerEnv(
|
|
260
|
+
function generateDockerEnv(
|
|
261
|
+
apps: DatabaseAppConfig[],
|
|
262
|
+
eventsBackend?: EventsBackend,
|
|
263
|
+
): string {
|
|
213
264
|
const envVars = apps.map((app) => {
|
|
214
265
|
const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
|
|
215
266
|
return `${envVar}=${app.password}`;
|
|
216
267
|
});
|
|
217
268
|
|
|
269
|
+
// Add pgboss password if events backend is pgboss
|
|
270
|
+
if (eventsBackend === 'pgboss') {
|
|
271
|
+
envVars.push(`PGBOSS_DB_PASSWORD=pgboss-dev-password`);
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
return `# Auto-generated docker environment file
|
|
219
275
|
# Contains database passwords for docker-compose postgres init
|
|
220
276
|
# This file is gitignored - do not commit to version control
|
|
@@ -223,12 +279,16 @@ ${envVars.join('\n')}
|
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
/**
|
|
226
|
-
* Generate PostgreSQL init shell script that creates per-app users with separate schemas
|
|
227
|
-
* Uses
|
|
282
|
+
* Generate PostgreSQL init shell script that creates per-app users with separate schemas.
|
|
283
|
+
* Uses idempotent DO blocks so the script can be re-run safely.
|
|
228
284
|
* - api user: uses public schema
|
|
229
285
|
* - auth user: uses auth schema with search_path=auth
|
|
286
|
+
* - pgboss user: uses pgboss schema (when events === 'pgboss')
|
|
230
287
|
*/
|
|
231
|
-
function generatePostgresInitScript(
|
|
288
|
+
function generatePostgresInitScript(
|
|
289
|
+
apps: DatabaseAppConfig[],
|
|
290
|
+
eventsBackend?: EventsBackend,
|
|
291
|
+
): string {
|
|
232
292
|
const userCreations = apps.map((app) => {
|
|
233
293
|
const userName = app.name.replace(/-/g, '_');
|
|
234
294
|
const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
|
|
@@ -236,25 +296,39 @@ function generatePostgresInitScript(apps: DatabaseAppConfig[]): string {
|
|
|
236
296
|
const schemaName = isApi ? 'public' : userName;
|
|
237
297
|
|
|
238
298
|
if (isApi) {
|
|
239
|
-
// API user uses public schema
|
|
240
299
|
return `
|
|
241
|
-
# Create ${app.name} user (uses public schema)
|
|
300
|
+
# Create ${app.name} user (uses public schema) - idempotent
|
|
242
301
|
echo "Creating user ${userName}..."
|
|
243
302
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
244
|
-
|
|
303
|
+
DO \\$\\$
|
|
304
|
+
BEGIN
|
|
305
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
|
|
306
|
+
CREATE USER ${userName} WITH PASSWORD '$${envVar}';
|
|
307
|
+
ELSE
|
|
308
|
+
ALTER USER ${userName} WITH PASSWORD '$${envVar}';
|
|
309
|
+
END IF;
|
|
310
|
+
END
|
|
311
|
+
\\$\\$;
|
|
245
312
|
GRANT ALL ON SCHEMA public TO ${userName};
|
|
246
313
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${userName};
|
|
247
314
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${userName};
|
|
248
315
|
EOSQL
|
|
249
316
|
`;
|
|
250
317
|
}
|
|
251
|
-
// Other users get their own schema with search_path
|
|
252
318
|
return `
|
|
253
|
-
# Create ${app.name} user with dedicated schema
|
|
319
|
+
# Create ${app.name} user with dedicated schema - idempotent
|
|
254
320
|
echo "Creating user ${userName} with schema ${schemaName}..."
|
|
255
321
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
256
|
-
|
|
257
|
-
|
|
322
|
+
DO \\$\\$
|
|
323
|
+
BEGIN
|
|
324
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${userName}') THEN
|
|
325
|
+
CREATE USER ${userName} WITH PASSWORD '$${envVar}';
|
|
326
|
+
ELSE
|
|
327
|
+
ALTER USER ${userName} WITH PASSWORD '$${envVar}';
|
|
328
|
+
END IF;
|
|
329
|
+
END
|
|
330
|
+
\\$\\$;
|
|
331
|
+
CREATE SCHEMA IF NOT EXISTS ${schemaName} AUTHORIZATION ${userName};
|
|
258
332
|
ALTER USER ${userName} SET search_path TO ${schemaName};
|
|
259
333
|
GRANT USAGE ON SCHEMA ${schemaName} TO ${userName};
|
|
260
334
|
GRANT ALL ON ALL TABLES IN SCHEMA ${schemaName} TO ${userName};
|
|
@@ -265,14 +339,52 @@ EOSQL
|
|
|
265
339
|
`;
|
|
266
340
|
});
|
|
267
341
|
|
|
342
|
+
// Add pgboss user and schema if events backend is pgboss
|
|
343
|
+
let pgbossBlock = '';
|
|
344
|
+
if (eventsBackend === 'pgboss') {
|
|
345
|
+
pgbossBlock = `
|
|
346
|
+
# Create pgboss user with dedicated schema - idempotent
|
|
347
|
+
echo "Creating pgboss user and schema..."
|
|
348
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
349
|
+
DO \\$\\$
|
|
350
|
+
BEGIN
|
|
351
|
+
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgboss') THEN
|
|
352
|
+
CREATE USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
|
|
353
|
+
ELSE
|
|
354
|
+
ALTER USER pgboss WITH PASSWORD '$PGBOSS_DB_PASSWORD';
|
|
355
|
+
END IF;
|
|
356
|
+
END
|
|
357
|
+
\\$\\$;
|
|
358
|
+
CREATE SCHEMA IF NOT EXISTS pgboss AUTHORIZATION pgboss;
|
|
359
|
+
ALTER USER pgboss SET search_path TO pgboss;
|
|
360
|
+
GRANT USAGE ON SCHEMA pgboss TO pgboss;
|
|
361
|
+
GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO pgboss;
|
|
362
|
+
GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO pgboss;
|
|
363
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON TABLES TO pgboss;
|
|
364
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA pgboss GRANT ALL ON SEQUENCES TO pgboss;
|
|
365
|
+
EOSQL
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Add extensions
|
|
370
|
+
const extensions = `
|
|
371
|
+
# Create extensions
|
|
372
|
+
echo "Creating extensions..."
|
|
373
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
374
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
375
|
+
CREATE EXTENSION IF NOT EXISTS citext;
|
|
376
|
+
EOSQL
|
|
377
|
+
`;
|
|
378
|
+
|
|
268
379
|
return `#!/bin/bash
|
|
269
380
|
set -e
|
|
270
381
|
|
|
271
|
-
# Auto-generated PostgreSQL init script
|
|
382
|
+
# Auto-generated PostgreSQL init script (idempotent - safe to re-run)
|
|
272
383
|
# Creates per-app users with separate schemas in a single database
|
|
273
384
|
# - api: uses public schema
|
|
274
|
-
# - auth: uses auth schema (search_path=auth)
|
|
275
|
-
${
|
|
385
|
+
# - auth: uses auth schema (search_path=auth)${eventsBackend === 'pgboss' ? '\n# - pgboss: uses pgboss schema for event processing' : ''}
|
|
386
|
+
${extensions}
|
|
387
|
+
${userCreations.join('\n')}${pgbossBlock}
|
|
276
388
|
echo "Database initialization complete!"
|
|
277
389
|
`;
|
|
278
390
|
}
|
package/src/init/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
generateDbPassword,
|
|
10
10
|
generateDbUrl,
|
|
11
11
|
} from '../setup/fullstack-secrets.js';
|
|
12
|
-
import type { ComposeServiceName } from '../types.js';
|
|
12
|
+
import type { ComposeServiceName, EventsBackend } from '../types.js';
|
|
13
13
|
import { generateAuthAppFiles } from './generators/auth.js';
|
|
14
14
|
import { generateConfigFiles } from './generators/config.js';
|
|
15
15
|
import {
|
|
@@ -27,6 +27,7 @@ import { generateWebAppFiles } from './generators/web.js';
|
|
|
27
27
|
import {
|
|
28
28
|
type DeployTarget,
|
|
29
29
|
deployTargetChoices,
|
|
30
|
+
eventsBackendChoices,
|
|
30
31
|
getTemplate,
|
|
31
32
|
isFullstackTemplate,
|
|
32
33
|
loggerTypeChoices,
|
|
@@ -110,6 +111,13 @@ export async function initCommand(
|
|
|
110
111
|
choices: servicesChoices.map((c) => ({ ...c, selected: true })),
|
|
111
112
|
hint: '- Space to select. Return to submit',
|
|
112
113
|
},
|
|
114
|
+
{
|
|
115
|
+
type: options.yes ? null : 'select',
|
|
116
|
+
name: 'eventsBackend',
|
|
117
|
+
message: 'Event backend:',
|
|
118
|
+
choices: eventsBackendChoices,
|
|
119
|
+
initial: 0,
|
|
120
|
+
},
|
|
113
121
|
{
|
|
114
122
|
type: options.yes ? null : 'select',
|
|
115
123
|
name: 'packageManager',
|
|
@@ -183,13 +191,27 @@ export async function initCommand(
|
|
|
183
191
|
const servicesArray: string[] = options.yes
|
|
184
192
|
? ['db', 'cache', 'mail', 'storage']
|
|
185
193
|
: answers.services || [];
|
|
194
|
+
|
|
195
|
+
// Determine events backend (default to pgboss for fullstack with --yes)
|
|
196
|
+
const eventsBackend: EventsBackend | undefined = options.yes
|
|
197
|
+
? isFullstack
|
|
198
|
+
? 'pgboss'
|
|
199
|
+
: undefined
|
|
200
|
+
: answers.eventsBackend;
|
|
201
|
+
|
|
186
202
|
const services: ServicesSelection = {
|
|
187
203
|
db: servicesArray.includes('db'),
|
|
188
204
|
cache: servicesArray.includes('cache'),
|
|
189
205
|
mail: servicesArray.includes('mail'),
|
|
190
206
|
storage: servicesArray.includes('storage'),
|
|
207
|
+
events: eventsBackend,
|
|
191
208
|
};
|
|
192
209
|
|
|
210
|
+
// pgboss requires postgres
|
|
211
|
+
if (services.events === 'pgboss') {
|
|
212
|
+
services.db = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
193
215
|
const pkgManager: PackageManager = options.pm
|
|
194
216
|
? options.pm
|
|
195
217
|
: options.yes
|
|
@@ -333,9 +355,12 @@ export async function initCommand(
|
|
|
333
355
|
if (services.cache) secretServices.push('redis');
|
|
334
356
|
if (services.storage) secretServices.push('minio');
|
|
335
357
|
if (services.mail) secretServices.push('mailpit');
|
|
358
|
+
if (services.events === 'sns') secretServices.push('localstack');
|
|
359
|
+
if (services.events === 'rabbitmq') secretServices.push('rabbitmq');
|
|
336
360
|
|
|
337
361
|
const devSecrets = createStageSecrets('development', secretServices, {
|
|
338
362
|
projectName: name,
|
|
363
|
+
eventsBackend: services.events,
|
|
339
364
|
});
|
|
340
365
|
|
|
341
366
|
// Add common custom secrets
|
|
@@ -39,6 +39,7 @@ export interface ServicesSelection {
|
|
|
39
39
|
cache: boolean;
|
|
40
40
|
mail: boolean;
|
|
41
41
|
storage: boolean;
|
|
42
|
+
events?: import('../../types.js').EventsBackend;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -255,6 +256,33 @@ export const servicesChoices = [
|
|
|
255
256
|
},
|
|
256
257
|
];
|
|
257
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Event backend choices for prompts
|
|
261
|
+
*/
|
|
262
|
+
export const eventsBackendChoices = [
|
|
263
|
+
{
|
|
264
|
+
title: 'pg-boss',
|
|
265
|
+
value: 'pgboss' as const,
|
|
266
|
+
description:
|
|
267
|
+
'PostgreSQL-based job queue (reuses postgres, no extra container)',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
title: 'SNS/SQS',
|
|
271
|
+
value: 'sns' as const,
|
|
272
|
+
description: 'AWS SNS+SQS via LocalStack for local dev',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
title: 'RabbitMQ',
|
|
276
|
+
value: 'rabbitmq' as const,
|
|
277
|
+
description: 'AMQP message broker',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
title: 'None',
|
|
281
|
+
value: undefined,
|
|
282
|
+
description: 'Skip event backend',
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
|
|
258
286
|
/**
|
|
259
287
|
* Get a template by name
|
|
260
288
|
*/
|
package/src/init/versions.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
35
35
|
'@geekmidas/constructs': '~3.0.2',
|
|
36
36
|
'@geekmidas/db': '~1.0.0',
|
|
37
37
|
'@geekmidas/emailkit': '~1.0.0',
|
|
38
|
-
'@geekmidas/envkit': '~1.0.
|
|
38
|
+
'@geekmidas/envkit': '~1.0.4',
|
|
39
39
|
'@geekmidas/errors': '~1.0.0',
|
|
40
40
|
'@geekmidas/events': '~1.1.0',
|
|
41
41
|
'@geekmidas/logger': '~1.0.0',
|
|
@@ -2,7 +2,11 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
createStageSecrets,
|
|
4
4
|
generateConnectionUrls,
|
|
5
|
+
generateEventConnectionStrings,
|
|
6
|
+
generateLocalStackAccessKeyId,
|
|
7
|
+
generateLocalStackCredentials,
|
|
5
8
|
generateMinioEndpoint,
|
|
9
|
+
generatePgBossUrl,
|
|
6
10
|
generatePostgresUrl,
|
|
7
11
|
generateRabbitmqUrl,
|
|
8
12
|
generateRedisUrl,
|
|
@@ -385,3 +389,182 @@ describe('rotateServicePassword', () => {
|
|
|
385
389
|
expect(rotated.services.rabbitmq).toEqual(original.services.rabbitmq);
|
|
386
390
|
});
|
|
387
391
|
});
|
|
392
|
+
|
|
393
|
+
describe('generateLocalStackAccessKeyId', () => {
|
|
394
|
+
it('should start with LSIA prefix', () => {
|
|
395
|
+
const keyId = generateLocalStackAccessKeyId();
|
|
396
|
+
expect(keyId).toMatch(/^LSIA/);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should be at least 20 characters', () => {
|
|
400
|
+
const keyId = generateLocalStackAccessKeyId();
|
|
401
|
+
expect(keyId.length).toBeGreaterThanOrEqual(20);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should generate unique keys', () => {
|
|
405
|
+
const key1 = generateLocalStackAccessKeyId();
|
|
406
|
+
const key2 = generateLocalStackAccessKeyId();
|
|
407
|
+
expect(key1).not.toBe(key2);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('generatePgBossUrl', () => {
|
|
412
|
+
it('should generate pgboss connection URL', () => {
|
|
413
|
+
const creds: ServiceCredentials = {
|
|
414
|
+
host: 'localhost',
|
|
415
|
+
port: 5432,
|
|
416
|
+
username: 'pgboss',
|
|
417
|
+
password: 'secret',
|
|
418
|
+
database: 'myapp_dev',
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const url = generatePgBossUrl(creds);
|
|
422
|
+
expect(url).toBe(
|
|
423
|
+
'pgboss://pgboss:secret@localhost:5432/myapp_dev?schema=pgboss',
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should encode special characters in password', () => {
|
|
428
|
+
const creds: ServiceCredentials = {
|
|
429
|
+
host: 'localhost',
|
|
430
|
+
port: 5432,
|
|
431
|
+
username: 'pgboss',
|
|
432
|
+
password: 'p@ss/word',
|
|
433
|
+
database: 'app',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const url = generatePgBossUrl(creds);
|
|
437
|
+
expect(url).toContain('p%40ss%2Fword');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('generateLocalStackCredentials', () => {
|
|
442
|
+
it('should generate credentials with LSIA-prefixed access key', () => {
|
|
443
|
+
const creds = generateLocalStackCredentials();
|
|
444
|
+
expect(creds.accessKeyId).toMatch(/^LSIA/);
|
|
445
|
+
expect(creds.host).toBe('localhost');
|
|
446
|
+
expect(creds.port).toBe(4566);
|
|
447
|
+
expect(creds.region).toBe('us-east-1');
|
|
448
|
+
expect(creds.password).toHaveLength(32);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('generateEventConnectionStrings', () => {
|
|
453
|
+
it('should generate pgboss connection strings', () => {
|
|
454
|
+
const services: StageSecrets['services'] = {
|
|
455
|
+
pgboss: {
|
|
456
|
+
host: 'localhost',
|
|
457
|
+
port: 5432,
|
|
458
|
+
username: 'pgboss',
|
|
459
|
+
password: 'secret',
|
|
460
|
+
database: 'myapp_dev',
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const result = generateEventConnectionStrings('pgboss', services);
|
|
465
|
+
expect(result.publisher).toContain('pgboss://');
|
|
466
|
+
expect(result.subscriber).toContain('pgboss://');
|
|
467
|
+
expect(result.publisher).toBe(result.subscriber);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should generate sns/sqs connection strings', () => {
|
|
471
|
+
const services: StageSecrets['services'] = {
|
|
472
|
+
localstack: {
|
|
473
|
+
host: 'localhost',
|
|
474
|
+
port: 4566,
|
|
475
|
+
username: 'localstack',
|
|
476
|
+
password: 'secret',
|
|
477
|
+
accessKeyId: 'LSIAtest1234567890xx',
|
|
478
|
+
region: 'us-east-1',
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const result = generateEventConnectionStrings('sns', services);
|
|
483
|
+
expect(result.publisher).toContain('sns://');
|
|
484
|
+
expect(result.subscriber).toContain('sqs://');
|
|
485
|
+
expect(result.publisher).toContain('LSIAtest1234567890xx');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should generate rabbitmq connection strings', () => {
|
|
489
|
+
const services: StageSecrets['services'] = {
|
|
490
|
+
rabbitmq: {
|
|
491
|
+
host: 'localhost',
|
|
492
|
+
port: 5672,
|
|
493
|
+
username: 'app',
|
|
494
|
+
password: 'secret',
|
|
495
|
+
vhost: '/',
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const result = generateEventConnectionStrings('rabbitmq', services);
|
|
500
|
+
expect(result.publisher).toContain('amqp://');
|
|
501
|
+
expect(result.subscriber).toContain('amqp://');
|
|
502
|
+
expect(result.publisher).toBe(result.subscriber);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should throw if pgboss credentials missing', () => {
|
|
506
|
+
expect(() => generateEventConnectionStrings('pgboss', {})).toThrow(
|
|
507
|
+
'pgboss credentials required',
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should throw if localstack credentials missing', () => {
|
|
512
|
+
expect(() => generateEventConnectionStrings('sns', {})).toThrow(
|
|
513
|
+
'localstack credentials required',
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('createStageSecrets with events', () => {
|
|
519
|
+
it('should create pgboss credentials when eventsBackend is pgboss', () => {
|
|
520
|
+
const secrets = createStageSecrets('development', ['postgres'], {
|
|
521
|
+
eventsBackend: 'pgboss',
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
expect(secrets.eventsBackend).toBe('pgboss');
|
|
525
|
+
expect(secrets.services.pgboss).toBeDefined();
|
|
526
|
+
expect(secrets.services.pgboss!.username).toBe('pgboss');
|
|
527
|
+
expect(secrets.services.pgboss!.host).toBe(secrets.services.postgres!.host);
|
|
528
|
+
expect(secrets.services.pgboss!.database).toBe(
|
|
529
|
+
secrets.services.postgres!.database,
|
|
530
|
+
);
|
|
531
|
+
expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain(
|
|
532
|
+
'pgboss://',
|
|
533
|
+
);
|
|
534
|
+
expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain(
|
|
535
|
+
'pgboss://',
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should create localstack credentials when eventsBackend is sns', () => {
|
|
540
|
+
const secrets = createStageSecrets('development', [], {
|
|
541
|
+
eventsBackend: 'sns',
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
expect(secrets.eventsBackend).toBe('sns');
|
|
545
|
+
expect(secrets.services.localstack).toBeDefined();
|
|
546
|
+
expect(secrets.services.localstack!.accessKeyId).toMatch(/^LSIA/);
|
|
547
|
+
expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain('sns://');
|
|
548
|
+
expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain('sqs://');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should use rabbitmq credentials when eventsBackend is rabbitmq', () => {
|
|
552
|
+
const secrets = createStageSecrets('development', ['rabbitmq'], {
|
|
553
|
+
eventsBackend: 'rabbitmq',
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
expect(secrets.eventsBackend).toBe('rabbitmq');
|
|
557
|
+
expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toContain('amqp://');
|
|
558
|
+
expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toContain(
|
|
559
|
+
'amqp://',
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should not create event URLs without eventsBackend', () => {
|
|
564
|
+
const secrets = createStageSecrets('development', ['postgres']);
|
|
565
|
+
|
|
566
|
+
expect(secrets.eventsBackend).toBeUndefined();
|
|
567
|
+
expect(secrets.urls.EVENT_PUBLISHER_CONNECTION_STRING).toBeUndefined();
|
|
568
|
+
expect(secrets.urls.EVENT_SUBSCRIBER_CONNECTION_STRING).toBeUndefined();
|
|
569
|
+
});
|
|
570
|
+
});
|