Skip to content

Commit 7851306

Browse files
authored
Merge pull request #5 from pipedrive/AINATIVEM-watch-mode
Use tsx watch mode and inject DB URL into .env.example
2 parents 7331809 + ce801dd commit 7851306

4 files changed

Lines changed: 82 additions & 12 deletions

File tree

src/generators/node/database.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ describe('generateDatabase — src/database/index.ts', () => {
7272
const content = await read('src/database/index.ts');
7373
expect(content).toContain('postgres');
7474
expect(content).toContain('drizzle-orm/postgres-js');
75+
expect(content).toContain('onnotice');
76+
expect(content).toContain("'42P06'");
77+
expect(content).toContain("'42P07'");
7578
expect(content).toContain('export');
7679
});
7780

@@ -81,6 +84,7 @@ describe('generateDatabase — src/database/index.ts', () => {
8184
const content = await read('src/database/index.ts');
8285
expect(content).toContain('mysql2');
8386
expect(content).toContain('drizzle-orm/mysql2');
87+
expect(content).toContain("mode: 'default'");
8488
});
8589

8690
it('sqlite client uses @libsql/client', async () => {
@@ -218,6 +222,7 @@ describe('generateDatabase — docker-compose.yml', () => {
218222
expect(await exists(join(tmpDir, 'docker-compose.yml'))).toBe(true);
219223
const content = await read('docker-compose.yml');
220224
expect(content).toContain('postgres:16');
225+
expect(content).toContain('postgres_data:/var/lib/postgresql/data');
221226
expect(content).toContain('pg_isready');
222227
expect(content).toContain('healthcheck');
223228
});
@@ -227,6 +232,9 @@ describe('generateDatabase — docker-compose.yml', () => {
227232
await generateDatabase(tmpDir, mysqlOptions);
228233
const content = await read('docker-compose.yml');
229234
expect(content).toContain('mysql:8');
235+
expect(content).toContain('127.0.0.1:3307:3306');
236+
expect(content).not.toContain('3306:3306');
237+
expect(content).toContain('mysql_data:/var/lib/mysql');
230238
expect(content).toContain('mysqladmin');
231239
expect(content).toContain('healthcheck');
232240
});

src/generators/node/database.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ function dbClientContent(database: GeneratorOptions['database']): string {
110110
import postgres from 'postgres';
111111
import * as schema from './schema.js';
112112
113-
const client = postgres(process.env.DATABASE_URL!);
113+
const client = postgres(process.env.DATABASE_URL!, {
114+
onnotice: (notice) => {
115+
if (notice.code === '42P06' || notice.code === '42P07') return;
116+
console.warn(notice);
117+
},
118+
});
114119
export const db = drizzle(client, { schema });
115120
`;
116121
}
@@ -122,7 +127,7 @@ function dbClientContent(database: GeneratorOptions['database']): string {
122127
import * as schema from './schema.js';
123128
124129
const pool = mysql.createPool(process.env.DATABASE_URL!);
125-
export const db = drizzle(pool, { schema });
130+
export const db = drizzle(pool, { schema, mode: 'default' });
126131
`;
127132
}
128133

@@ -264,15 +269,15 @@ async function generateDockerCompose(outputDir: string, options: GeneratorOption
264269
ports:
265270
- '5432:5432'
266271
volumes:
267-
- db_data:/var/lib/postgresql/data
272+
- postgres_data:/var/lib/postgresql/data
268273
healthcheck:
269274
test: ['CMD', 'pg_isready', '-U', 'app']
270275
interval: 5s
271276
timeout: 5s
272277
retries: 5
273278
274279
volumes:
275-
db_data:
280+
postgres_data:
276281
`
277282
: dedent`
278283
services:
@@ -284,17 +289,17 @@ async function generateDockerCompose(outputDir: string, options: GeneratorOption
284289
MYSQL_USER: app
285290
MYSQL_PASSWORD: app
286291
ports:
287-
- '3306:3306'
292+
- '127.0.0.1:3307:3306'
288293
volumes:
289-
- db_data:/var/lib/mysql
294+
- mysql_data:/var/lib/mysql
290295
healthcheck:
291296
test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'app', '--password=app']
292297
interval: 5s
293298
timeout: 5s
294299
retries: 5
295300
296301
volumes:
297-
db_data:
302+
mysql_data:
298303
`;
299304
await writeFile(join(outputDir, 'docker-compose.yml'), content);
300305
}

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: 29 additions & 5 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
});
@@ -88,7 +107,7 @@ class PackageJsonStep implements BuildStep {
88107
version: '0.1.0',
89108
type: 'module',
90109
scripts: {
91-
'dev': 'tsx src/index.ts',
110+
'dev': 'tsx watch --env-file=.env src/index.ts',
92111
'build': 'tsc',
93112
'typecheck': 'tsc --noEmit',
94113
'db:migrate': 'drizzle-kit migrate',
@@ -132,14 +151,19 @@ class TsConfigStep implements BuildStep {
132151
}
133152

134153
class EnvExampleStep implements BuildStep {
135-
async execute(outputDir: string, _options: GeneratorOptions): Promise<void> {
154+
async execute(outputDir: string, options: GeneratorOptions): Promise<void> {
155+
const databaseUrlExample: Record<GeneratorOptions['database'], string> = {
156+
postgres: `postgresql://app:app@localhost:5432/${options.projectName}`,
157+
mysql: `mysql://app:app@localhost:3307/${options.projectName}`,
158+
sqlite: 'file:./data.db',
159+
};
136160
await writeFile(
137161
join(outputDir, '.env.example'),
138162
dedent`
139163
PIPEDRIVE_CLIENT_ID=
140164
PIPEDRIVE_CLIENT_SECRET=
141165
PIPEDRIVE_REDIRECT_URI=http://localhost:3000/oauth/callback
142-
DATABASE_URL=
166+
DATABASE_URL=${databaseUrlExample[options.database]}
143167
PORT=3000
144168
`,
145169
);

0 commit comments

Comments
 (0)