@geekmidas/cli 1.10.15 → 1.10.16
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 +6 -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 +322 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +322 -46
- 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 +3 -3
- package/src/dev/__tests__/entry.spec.ts +3 -5
- package/src/dev/__tests__/index.spec.ts +5 -5
- package/src/dev/index.ts +10 -10
- 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.16",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"yaml": "~2.8.2",
|
|
59
59
|
"@geekmidas/envkit": "~1.0.3",
|
|
60
60
|
"@geekmidas/constructs": "~3.0.2",
|
|
61
|
+
"@geekmidas/logger": "~1.0.0",
|
|
61
62
|
"@geekmidas/schema": "~1.0.0",
|
|
62
|
-
"@geekmidas/errors": "~1.0.0"
|
|
63
|
-
"@geekmidas/logger": "~1.0.0"
|
|
63
|
+
"@geekmidas/errors": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/lodash.kebabcase": "^4.1.9",
|
|
@@ -97,11 +97,9 @@ describe('createEntryWrapper', () => {
|
|
|
97
97
|
|
|
98
98
|
const content = await readFile(wrapperPath, 'utf-8');
|
|
99
99
|
|
|
100
|
-
expect(content).toContain(
|
|
101
|
-
"import { Credentials } from '@geekmidas/envkit/credentials'",
|
|
102
|
-
);
|
|
100
|
+
expect(content).toContain('globalThis.__gkm_credentials__');
|
|
103
101
|
expect(content).toContain(secretsPath);
|
|
104
|
-
expect(content).toContain('Object.assign(
|
|
102
|
+
expect(content).toContain('Object.assign(process.env');
|
|
105
103
|
expect(content).toContain("await import('/path/to/entry.ts')");
|
|
106
104
|
});
|
|
107
105
|
|
|
@@ -114,7 +112,7 @@ describe('createEntryWrapper', () => {
|
|
|
114
112
|
|
|
115
113
|
const content = await readFile(wrapperPath, 'utf-8');
|
|
116
114
|
|
|
117
|
-
const credentialsIndex = content.indexOf('
|
|
115
|
+
const credentialsIndex = content.indexOf('__gkm_credentials__');
|
|
118
116
|
const importIndex = content.indexOf("await import('/path/to/entry.ts')");
|
|
119
117
|
|
|
120
118
|
expect(credentialsIndex).toBeGreaterThan(-1);
|
|
@@ -1799,23 +1799,23 @@ describe('generateServerEntryContent', () => {
|
|
|
1799
1799
|
expect(content).not.toMatch(/^import.*createApp/m);
|
|
1800
1800
|
});
|
|
1801
1801
|
|
|
1802
|
-
it('should inject
|
|
1802
|
+
it('should inject credentials via globalThis before dynamic import', () => {
|
|
1803
1803
|
const content = generateServerEntryContent({
|
|
1804
1804
|
secretsJsonPath: '/tmp/dev-secrets.json',
|
|
1805
1805
|
});
|
|
1806
1806
|
|
|
1807
|
-
const
|
|
1807
|
+
const credentialsIdx = content.indexOf('__gkm_credentials__');
|
|
1808
1808
|
const dynamicImportIdx = content.indexOf("await import('./app.js')");
|
|
1809
1809
|
|
|
1810
|
-
expect(
|
|
1810
|
+
expect(credentialsIdx).toBeGreaterThan(-1);
|
|
1811
1811
|
expect(dynamicImportIdx).toBeGreaterThan(-1);
|
|
1812
|
-
expect(
|
|
1812
|
+
expect(credentialsIdx).toBeLessThan(dynamicImportIdx);
|
|
1813
1813
|
});
|
|
1814
1814
|
|
|
1815
1815
|
it('should not include credentials injection when no secrets path', () => {
|
|
1816
1816
|
const content = generateServerEntryContent({});
|
|
1817
1817
|
|
|
1818
|
-
expect(content).not.toContain('
|
|
1818
|
+
expect(content).not.toContain('__gkm_credentials__');
|
|
1819
1819
|
expect(content).not.toContain('existsSync');
|
|
1820
1820
|
});
|
|
1821
1821
|
|
package/src/dev/index.ts
CHANGED
|
@@ -1623,17 +1623,16 @@ export function findSecretsRoot(startDir: string): string {
|
|
|
1623
1623
|
* @internal
|
|
1624
1624
|
*/
|
|
1625
1625
|
function generateCredentialsInjection(secretsJsonPath: string): string {
|
|
1626
|
-
return `import {
|
|
1627
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1626
|
+
return `import { existsSync, readFileSync } from 'node:fs';
|
|
1628
1627
|
|
|
1629
|
-
// Inject dev secrets
|
|
1628
|
+
// Inject dev secrets via globalThis and process.env
|
|
1629
|
+
// Using globalThis.__gkm_credentials__ avoids CJS/ESM interop issues where
|
|
1630
|
+
// Object.assign on the Credentials export only mutates one module copy.
|
|
1630
1631
|
const secretsPath = '${secretsJsonPath}';
|
|
1631
1632
|
if (existsSync(secretsPath)) {
|
|
1632
1633
|
const secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1633
|
-
|
|
1634
|
+
globalThis.__gkm_credentials__ = secrets;
|
|
1634
1635
|
Object.assign(process.env, secrets);
|
|
1635
|
-
// Debug: uncomment to verify preload is running
|
|
1636
|
-
// console.log('[gkm preload] Injected', Object.keys(secrets).length, 'credentials');
|
|
1637
1636
|
}
|
|
1638
1637
|
`;
|
|
1639
1638
|
}
|
|
@@ -1958,13 +1957,14 @@ export function generateServerEntryContent(options: {
|
|
|
1958
1957
|
} = options;
|
|
1959
1958
|
|
|
1960
1959
|
const credentialsInjection = secretsJsonPath
|
|
1961
|
-
? `import {
|
|
1962
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1960
|
+
? `import { existsSync, readFileSync } from 'node:fs';
|
|
1963
1961
|
|
|
1964
|
-
// Inject dev secrets
|
|
1962
|
+
// Inject dev secrets via globalThis (must happen before app import)
|
|
1965
1963
|
const secretsPath = '${secretsJsonPath}';
|
|
1966
1964
|
if (existsSync(secretsPath)) {
|
|
1967
|
-
|
|
1965
|
+
const __secrets = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
1966
|
+
globalThis.__gkm_credentials__ = __secrets;
|
|
1967
|
+
Object.assign(process.env, __secrets);
|
|
1968
1968
|
}
|
|
1969
1969
|
|
|
1970
1970
|
`
|
package/src/docker/compose.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const DEFAULT_SERVICE_IMAGES: Record<ComposeServiceName, string> = {
|
|
|
15
15
|
rabbitmq: 'rabbitmq',
|
|
16
16
|
minio: 'minio/minio',
|
|
17
17
|
mailpit: 'axllent/mailpit',
|
|
18
|
+
localstack: 'localstack/localstack',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/** Default Docker image versions for services */
|
|
@@ -24,6 +25,7 @@ export const DEFAULT_SERVICE_VERSIONS: Record<ComposeServiceName, string> = {
|
|
|
24
25
|
rabbitmq: '3-management-alpine',
|
|
25
26
|
minio: 'latest',
|
|
26
27
|
mailpit: 'latest',
|
|
28
|
+
localstack: 'latest',
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export interface ComposeOptions {
|
|
@@ -145,6 +147,14 @@ services:
|
|
|
145
147
|
`;
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
if (serviceMap.has('localstack')) {
|
|
151
|
+
yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
|
|
152
|
+
- AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
153
|
+
- AWS_REGION=\${AWS_REGION:-us-east-1}
|
|
154
|
+
- AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
yaml += ` healthcheck:
|
|
149
159
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${port}${healthCheckPath}"]
|
|
150
160
|
interval: 30s
|
|
@@ -283,6 +293,32 @@ services:
|
|
|
283
293
|
`;
|
|
284
294
|
}
|
|
285
295
|
|
|
296
|
+
const localstackImage = serviceMap.get('localstack');
|
|
297
|
+
if (localstackImage) {
|
|
298
|
+
yaml += `
|
|
299
|
+
localstack:
|
|
300
|
+
image: ${localstackImage}
|
|
301
|
+
container_name: localstack
|
|
302
|
+
restart: unless-stopped
|
|
303
|
+
environment:
|
|
304
|
+
SERVICES: sns,sqs
|
|
305
|
+
AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
|
|
306
|
+
AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
|
|
307
|
+
AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
308
|
+
ports:
|
|
309
|
+
- "\${LOCALSTACK_PORT:-4566}:4566"
|
|
310
|
+
volumes:
|
|
311
|
+
- localstack_data:/var/lib/localstack
|
|
312
|
+
healthcheck:
|
|
313
|
+
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
|
314
|
+
interval: 10s
|
|
315
|
+
timeout: 5s
|
|
316
|
+
retries: 5
|
|
317
|
+
networks:
|
|
318
|
+
- app-network
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
286
322
|
// Add volumes
|
|
287
323
|
yaml += `
|
|
288
324
|
volumes:
|
|
@@ -308,6 +344,11 @@ volumes:
|
|
|
308
344
|
`;
|
|
309
345
|
}
|
|
310
346
|
|
|
347
|
+
if (serviceMap.has('localstack')) {
|
|
348
|
+
yaml += ` localstack_data:
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
|
|
311
352
|
// Add networks
|
|
312
353
|
yaml += `
|
|
313
354
|
networks:
|
|
@@ -384,6 +425,11 @@ export function generateWorkspaceCompose(
|
|
|
384
425
|
const hasRedis = services.cache !== undefined && services.cache !== false;
|
|
385
426
|
const hasMail = services.mail !== undefined && services.mail !== false;
|
|
386
427
|
const hasMinio = services.storage !== undefined && services.storage !== false;
|
|
428
|
+
const eventsBackend = services.events;
|
|
429
|
+
const hasLocalStack = eventsBackend === 'sns';
|
|
430
|
+
const hasRabbitMQ =
|
|
431
|
+
eventsBackend === 'rabbitmq' ||
|
|
432
|
+
(services as Record<string, unknown>).rabbitmq !== undefined;
|
|
387
433
|
|
|
388
434
|
// Get image versions from config
|
|
389
435
|
const postgresImage = getInfraServiceImage('postgres', services.db);
|
|
@@ -406,6 +452,7 @@ services:
|
|
|
406
452
|
hasRedis,
|
|
407
453
|
hasMinio,
|
|
408
454
|
hasMail,
|
|
455
|
+
eventsBackend,
|
|
409
456
|
});
|
|
410
457
|
}
|
|
411
458
|
|
|
@@ -497,6 +544,55 @@ services:
|
|
|
497
544
|
`;
|
|
498
545
|
}
|
|
499
546
|
|
|
547
|
+
if (hasLocalStack) {
|
|
548
|
+
yaml += `
|
|
549
|
+
localstack:
|
|
550
|
+
image: localstack/localstack:latest
|
|
551
|
+
container_name: ${workspace.name}-localstack
|
|
552
|
+
restart: unless-stopped
|
|
553
|
+
environment:
|
|
554
|
+
SERVICES: sns,sqs
|
|
555
|
+
AWS_DEFAULT_REGION: \${AWS_REGION:-us-east-1}
|
|
556
|
+
AWS_ACCESS_KEY_ID: \${AWS_ACCESS_KEY_ID:-localstack}
|
|
557
|
+
AWS_SECRET_ACCESS_KEY: \${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
558
|
+
ports:
|
|
559
|
+
- "\${LOCALSTACK_PORT:-4566}:4566"
|
|
560
|
+
volumes:
|
|
561
|
+
- localstack_data:/var/lib/localstack
|
|
562
|
+
healthcheck:
|
|
563
|
+
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
|
564
|
+
interval: 10s
|
|
565
|
+
timeout: 5s
|
|
566
|
+
retries: 5
|
|
567
|
+
networks:
|
|
568
|
+
- workspace-network
|
|
569
|
+
`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (hasRabbitMQ) {
|
|
573
|
+
yaml += `
|
|
574
|
+
rabbitmq:
|
|
575
|
+
image: rabbitmq:3-management-alpine
|
|
576
|
+
container_name: ${workspace.name}-rabbitmq
|
|
577
|
+
restart: unless-stopped
|
|
578
|
+
environment:
|
|
579
|
+
RABBITMQ_DEFAULT_USER: \${RABBITMQ_USER:-guest}
|
|
580
|
+
RABBITMQ_DEFAULT_PASS: \${RABBITMQ_PASSWORD:-guest}
|
|
581
|
+
ports:
|
|
582
|
+
- "\${RABBITMQ_HOST_PORT:-5672}:5672"
|
|
583
|
+
- "\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672"
|
|
584
|
+
volumes:
|
|
585
|
+
- rabbitmq_data:/var/lib/rabbitmq
|
|
586
|
+
healthcheck:
|
|
587
|
+
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
|
588
|
+
interval: 10s
|
|
589
|
+
timeout: 5s
|
|
590
|
+
retries: 5
|
|
591
|
+
networks:
|
|
592
|
+
- workspace-network
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
500
596
|
// Add volumes section
|
|
501
597
|
yaml += `
|
|
502
598
|
volumes:
|
|
@@ -517,6 +613,16 @@ volumes:
|
|
|
517
613
|
`;
|
|
518
614
|
}
|
|
519
615
|
|
|
616
|
+
if (hasLocalStack) {
|
|
617
|
+
yaml += ` localstack_data:
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (hasRabbitMQ) {
|
|
622
|
+
yaml += ` rabbitmq_data:
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
520
626
|
// Add networks section
|
|
521
627
|
yaml += `
|
|
522
628
|
networks:
|
|
@@ -575,10 +681,18 @@ function generateAppService(
|
|
|
575
681
|
hasRedis: boolean;
|
|
576
682
|
hasMinio: boolean;
|
|
577
683
|
hasMail: boolean;
|
|
684
|
+
eventsBackend?: import('../types').EventsBackend;
|
|
578
685
|
},
|
|
579
686
|
): string {
|
|
580
|
-
const {
|
|
581
|
-
|
|
687
|
+
const {
|
|
688
|
+
registry,
|
|
689
|
+
projectName,
|
|
690
|
+
hasPostgres,
|
|
691
|
+
hasRedis,
|
|
692
|
+
hasMinio,
|
|
693
|
+
hasMail,
|
|
694
|
+
eventsBackend,
|
|
695
|
+
} = options;
|
|
582
696
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
583
697
|
|
|
584
698
|
// Health check path - frontends use /, backends use /health
|
|
@@ -640,6 +754,18 @@ function generateAppService(
|
|
|
640
754
|
- MAIL_FROM=\${MAIL_FROM:-noreply@localhost}
|
|
641
755
|
`;
|
|
642
756
|
}
|
|
757
|
+
if (eventsBackend) {
|
|
758
|
+
yaml += ` - EVENT_PUBLISHER_CONNECTION_STRING=\${EVENT_PUBLISHER_CONNECTION_STRING}
|
|
759
|
+
- EVENT_SUBSCRIBER_CONNECTION_STRING=\${EVENT_SUBSCRIBER_CONNECTION_STRING}
|
|
760
|
+
`;
|
|
761
|
+
if (eventsBackend === 'sns') {
|
|
762
|
+
yaml += ` - AWS_ACCESS_KEY_ID=\${AWS_ACCESS_KEY_ID:-localstack}
|
|
763
|
+
- AWS_SECRET_ACCESS_KEY=\${AWS_SECRET_ACCESS_KEY:-localstack}
|
|
764
|
+
- AWS_REGION=\${AWS_REGION:-us-east-1}
|
|
765
|
+
- AWS_ENDPOINT_URL=\${AWS_ENDPOINT_URL:-http://localstack:4566}
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
643
769
|
}
|
|
644
770
|
|
|
645
771
|
yaml += ` healthcheck:
|
|
@@ -656,6 +782,8 @@ function generateAppService(
|
|
|
656
782
|
if (hasRedis) dependencies.push('redis');
|
|
657
783
|
if (hasMinio) dependencies.push('minio');
|
|
658
784
|
if (hasMail) dependencies.push('mailpit');
|
|
785
|
+
if (eventsBackend === 'sns') dependencies.push('localstack');
|
|
786
|
+
if (eventsBackend === 'rabbitmq') dependencies.push('rabbitmq');
|
|
659
787
|
}
|
|
660
788
|
|
|
661
789
|
if (dependencies.length > 0) {
|
|
@@ -295,6 +295,90 @@ describe('generateDockerFiles', () => {
|
|
|
295
295
|
expect(files[0].content).not.toContain('mailpit');
|
|
296
296
|
expect(files[0].content).not.toContain('minio');
|
|
297
297
|
});
|
|
298
|
+
|
|
299
|
+
it('should include localstack when events is sns', () => {
|
|
300
|
+
const options = {
|
|
301
|
+
...baseOptions,
|
|
302
|
+
services: {
|
|
303
|
+
db: true,
|
|
304
|
+
cache: true,
|
|
305
|
+
mail: false,
|
|
306
|
+
storage: false,
|
|
307
|
+
events: 'sns' as const,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
311
|
+
const compose = files[0].content;
|
|
312
|
+
expect(compose).toContain('localstack');
|
|
313
|
+
expect(compose).toContain('SERVICES: sns,sqs');
|
|
314
|
+
expect(compose).toContain('LOCALSTACK_PORT');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should include rabbitmq when events is rabbitmq', () => {
|
|
318
|
+
const options = {
|
|
319
|
+
...baseOptions,
|
|
320
|
+
services: {
|
|
321
|
+
db: true,
|
|
322
|
+
cache: true,
|
|
323
|
+
mail: false,
|
|
324
|
+
storage: false,
|
|
325
|
+
events: 'rabbitmq' as const,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
329
|
+
const compose = files[0].content;
|
|
330
|
+
expect(compose).toContain('rabbitmq');
|
|
331
|
+
expect(compose).toContain('RABBITMQ_HOST_PORT');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should not add extra container for pgboss events', () => {
|
|
335
|
+
const options = {
|
|
336
|
+
...baseOptions,
|
|
337
|
+
services: {
|
|
338
|
+
db: true,
|
|
339
|
+
cache: true,
|
|
340
|
+
mail: false,
|
|
341
|
+
storage: false,
|
|
342
|
+
events: 'pgboss' as const,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
const files = generateDockerFiles(options, minimalTemplate);
|
|
346
|
+
const compose = files[0].content;
|
|
347
|
+
expect(compose).not.toContain('localstack');
|
|
348
|
+
// pgboss reuses postgres, no separate container
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should generate idempotent postgres init script with pgboss', () => {
|
|
352
|
+
const options = {
|
|
353
|
+
...baseOptions,
|
|
354
|
+
template: 'fullstack' as const,
|
|
355
|
+
monorepo: true,
|
|
356
|
+
apiPath: 'apps/api',
|
|
357
|
+
services: {
|
|
358
|
+
db: true,
|
|
359
|
+
cache: true,
|
|
360
|
+
mail: false,
|
|
361
|
+
storage: false,
|
|
362
|
+
events: 'pgboss' as const,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const dbApps = [
|
|
366
|
+
{ name: 'api', password: 'api-pass' },
|
|
367
|
+
{ name: 'auth', password: 'auth-pass' },
|
|
368
|
+
];
|
|
369
|
+
const files = generateDockerFiles(options, apiTemplate, dbApps);
|
|
370
|
+
const initScript = files.find((f) => f.path === 'docker/postgres/init.sh');
|
|
371
|
+
expect(initScript).toBeDefined();
|
|
372
|
+
expect(initScript!.content).toContain('IF NOT EXISTS');
|
|
373
|
+
expect(initScript!.content).toContain('pgboss');
|
|
374
|
+
expect(initScript!.content).toContain('PGBOSS_DB_PASSWORD');
|
|
375
|
+
expect(initScript!.content).toContain('CREATE SCHEMA IF NOT EXISTS pgboss');
|
|
376
|
+
|
|
377
|
+
// Docker env should include pgboss password
|
|
378
|
+
const envFile = files.find((f) => f.path === 'docker/.env');
|
|
379
|
+
expect(envFile).toBeDefined();
|
|
380
|
+
expect(envFile!.content).toContain('PGBOSS_DB_PASSWORD');
|
|
381
|
+
});
|
|
298
382
|
});
|
|
299
383
|
|
|
300
384
|
describe('generateMonorepoFiles', () => {
|
|
@@ -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
|
}
|