Skip to content

Commit 0811b76

Browse files
committed
Improve generated database startup handling
1 parent 83896d5 commit 0811b76

4 files changed

Lines changed: 66 additions & 9 deletions

File tree

src/generators/node/database.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe('generateDatabase — src/database/index.ts', () => {
8181
const content = await read('src/database/index.ts');
8282
expect(content).toContain('mysql2');
8383
expect(content).toContain('drizzle-orm/mysql2');
84+
expect(content).toContain("mode: 'default'");
8485
});
8586

8687
it('sqlite client uses @libsql/client', async () => {
@@ -218,6 +219,7 @@ describe('generateDatabase — docker-compose.yml', () => {
218219
expect(await exists(join(tmpDir, 'docker-compose.yml'))).toBe(true);
219220
const content = await read('docker-compose.yml');
220221
expect(content).toContain('postgres:16');
222+
expect(content).toContain('postgres_data:/var/lib/postgresql/data');
221223
expect(content).toContain('pg_isready');
222224
expect(content).toContain('healthcheck');
223225
});
@@ -227,6 +229,9 @@ describe('generateDatabase — docker-compose.yml', () => {
227229
await generateDatabase(tmpDir, mysqlOptions);
228230
const content = await read('docker-compose.yml');
229231
expect(content).toContain('mysql:8');
232+
expect(content).toContain('127.0.0.1:3307:3306');
233+
expect(content).not.toContain('3306:3306');
234+
expect(content).toContain('mysql_data:/var/lib/mysql');
230235
expect(content).toContain('mysqladmin');
231236
expect(content).toContain('healthcheck');
232237
});

src/generators/node/database.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function dbClientContent(database: GeneratorOptions['database']): string {
122122
import * as schema from './schema.js';
123123
124124
const pool = mysql.createPool(process.env.DATABASE_URL!);
125-
export const db = drizzle(pool, { schema });
125+
export const db = drizzle(pool, { schema, mode: 'default' });
126126
`;
127127
}
128128

@@ -264,15 +264,15 @@ async function generateDockerCompose(outputDir: string, options: GeneratorOption
264264
ports:
265265
- '5432:5432'
266266
volumes:
267-
- db_data:/var/lib/postgresql/data
267+
- postgres_data:/var/lib/postgresql/data
268268
healthcheck:
269269
test: ['CMD', 'pg_isready', '-U', 'app']
270270
interval: 5s
271271
timeout: 5s
272272
retries: 5
273273
274274
volumes:
275-
db_data:
275+
postgres_data:
276276
`
277277
: dedent`
278278
services:
@@ -284,17 +284,17 @@ async function generateDockerCompose(outputDir: string, options: GeneratorOption
284284
MYSQL_USER: app
285285
MYSQL_PASSWORD: app
286286
ports:
287-
- '3306:3306'
287+
- '127.0.0.1:3307:3306'
288288
volumes:
289-
- db_data:/var/lib/mysql
289+
- mysql_data:/var/lib/mysql
290290
healthcheck:
291291
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'app', '--password=app']
292292
interval: 5s
293293
timeout: 5s
294294
retries: 5
295295
296296
volumes:
297-
db_data:
297+
mysql_data:
298298
`;
299299
await writeFile(join(outputDir, 'docker-compose.yml'), content);
300300
}

src/generators/node/projectBuilder.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { describe, expect, it } from 'vitest';
2+
import { readFile, rm } from 'node:fs/promises';
3+
import { join } from 'node:path';
4+
import { tmpdir } from 'node:os';
25
import type { GeneratorOptions } from '../interface.js';
36
import { NodeProjectBuilder } from './projectBuilder.js';
47
import type { BuildStep } from './projectBuilder.js';
@@ -10,6 +13,9 @@ const options: GeneratorOptions = {
1013
appExtensions: [],
1114
};
1215

16+
const tmpDir = join(tmpdir(), 'cpa-project-builder-test');
17+
const read = (p: string) => readFile(join(tmpDir, p), 'utf-8');
18+
1319
function spyStep(tracker: string[], label: string): BuildStep {
1420
return {
1521
execute: async () => {
@@ -50,4 +56,31 @@ describe('NodeProjectBuilder', () => {
5056
expect(builder.addStep(spyStep([], 'x'))).toBe(builder);
5157
expect(builder.when(false, () => {})).toBe(builder);
5258
});
59+
60+
it('generates MySQL env example with the non-default host port', async () => {
61+
await rm(tmpDir, { recursive: true, force: true });
62+
63+
await new NodeProjectBuilder(tmpDir, { ...options, database: 'mysql' }).addEnvExample().build();
64+
65+
const content = await read('.env.example');
66+
expect(content).toContain('DATABASE_URL=mysql://app:app@localhost:3307/test-app');
67+
expect(content).not.toContain('DATABASE_URL=mysql://app:app@localhost:3306/test-app');
68+
69+
await rm(tmpDir, { recursive: true, force: true });
70+
});
71+
72+
it('generates server entry that retries database startup', async () => {
73+
await rm(tmpDir, { recursive: true, force: true });
74+
75+
await new NodeProjectBuilder(tmpDir, options).addServerEntry().build();
76+
77+
const content = await read('src/index.ts');
78+
expect(content).toContain('STARTUP_RETRY_ATTEMPTS = 60');
79+
expect(content).toContain('STARTUP_RETRY_DELAY_MS = 1000');
80+
expect(content).toContain('async function waitForDatabase()');
81+
expect(content).toContain('await waitForDatabase()');
82+
expect(content).toContain('Database is not ready yet');
83+
84+
await rm(tmpDir, { recursive: true, force: true });
85+
});
5386
});

src/generators/node/projectBuilder.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,27 @@ class ServerEntryStep implements BuildStep {
5959
import app from './app.js';
6060
6161
const PORT = ${envVarAccess('PORT', '3000')};
62-
63-
await runMigrations();
62+
const STARTUP_RETRY_ATTEMPTS = 60;
63+
const STARTUP_RETRY_DELAY_MS = 1000;
64+
65+
async function waitForDatabase(): Promise<void> {
66+
for (let attempt = 1; attempt <= STARTUP_RETRY_ATTEMPTS; attempt++) {
67+
try {
68+
await runMigrations();
69+
return;
70+
} catch (error) {
71+
if (attempt === STARTUP_RETRY_ATTEMPTS) throw error;
72+
73+
const message = error instanceof Error ? error.message : String(error);
74+
console.warn(
75+
\`Database is not ready yet (\${attempt}/\${STARTUP_RETRY_ATTEMPTS}): \${message}\`,
76+
);
77+
await new Promise<void>((resolve) => setTimeout(resolve, STARTUP_RETRY_DELAY_MS));
78+
}
79+
}
80+
}
81+
82+
await waitForDatabase();
6483
app.listen(PORT, () => {
6584
console.log(\`Server running on port \${PORT}\`);
6685
});
@@ -135,7 +154,7 @@ class EnvExampleStep implements BuildStep {
135154
async execute(outputDir: string, options: GeneratorOptions): Promise<void> {
136155
const databaseUrlExample: Record<GeneratorOptions['database'], string> = {
137156
postgres: `postgresql://app:app@localhost:5432/${options.projectName}`,
138-
mysql: `mysql://app:app@localhost:3306/${options.projectName}`,
157+
mysql: `mysql://app:app@localhost:3307/${options.projectName}`,
139158
sqlite: 'file:./data.db',
140159
};
141160
await writeFile(

0 commit comments

Comments
 (0)