Skip to content

Commit 68623a2

Browse files
author
Lasim
committed
fix(database): implement safe database proxy for graceful startup handling
1 parent 8b0b3a9 commit 68623a2

File tree

7 files changed

+73
-29
lines changed

7 files changed

+73
-29
lines changed

services/backend/src/db/index.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,21 +465,54 @@ export async function initializeDatabase(logger: FastifyBaseLogger): Promise<boo
465465
}
466466

467467
/**
468-
* Get database instance
468+
* Create a safe database proxy that handles operations gracefully during startup
469+
*/
470+
function createSafeDbProxy(): AnyDatabase {
471+
const handler = {
472+
get(target: any, prop: string) {
473+
// Allow basic property access for type checking
474+
if (prop === 'constructor' || prop === 'toString' || prop === 'valueOf') {
475+
return target[prop];
476+
}
477+
478+
// For any database operation, throw a more descriptive error
479+
return () => {
480+
throw new Error('Database not available. Please complete the setup process at /setup first.');
481+
};
482+
}
483+
};
484+
485+
// Create a minimal proxy object that looks like a database but safely handles calls
486+
const mockDb = {
487+
select: () => { throw new Error('Database not available. Please complete the setup process at /setup first.'); },
488+
insert: () => { throw new Error('Database not available. Please complete the setup process at /setup first.'); },
489+
update: () => { throw new Error('Database not available. Please complete the setup process at /setup first.'); },
490+
delete: () => { throw new Error('Database not available. Please complete the setup process at /setup first.'); },
491+
$client: null
492+
};
493+
494+
return new Proxy(mockDb, handler) as AnyDatabase;
495+
}
496+
497+
/**
498+
* Get database instance with graceful startup handling
469499
*/
470500
export function getDb(): AnyDatabase {
471501
if (!dbInstance || !isDbInitialized) {
472-
throw new Error('Database not initialized. Call initializeDatabase() first.');
502+
// During startup, return a safe proxy instead of throwing immediately
503+
// This allows the server to start and routes to be registered
504+
return createSafeDbProxy();
473505
}
474506
return dbInstance;
475507
}
476508

477509
/**
478-
* Get database schema
510+
* Get database schema with graceful startup handling
479511
*/
480512
export function getSchema(): AnySchema {
481513
if (!dbSchema || !isDbInitialized) {
482-
throw new Error('Database schema not generated. Call initializeDatabase() first.');
514+
// During startup, generate a basic schema to allow server startup
515+
return generateSchema();
483516
}
484517
return dbSchema;
485518
}

services/backend/src/email/templates/welcome.pug

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ block content
2727
code deploystack login
2828

2929
p
30-
strong 3. Start using your team's MCP tools:
31-
br
32-
code deploystack start
33-
34-
p
35-
strong 4. Configure VS Code:
30+
strong 3. Configure VS Code:
3631
br
3732
| Replace your MCP config with our gateway endpoint:
3833
br

services/backend/src/routes/users/me/devices/delete.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ import {
1212
} from './schemas';
1313

1414
export default async function deleteDeviceRoute(server: FastifyInstance) {
15-
const db = getDb();
16-
const deviceService = new DeviceService(db);
17-
1815
server.delete('/users/me/devices/:deviceId', {
1916
preValidation: requireAuthentication(),
2017
schema: {
@@ -44,6 +41,8 @@ export default async function deleteDeviceRoute(server: FastifyInstance) {
4441
}
4542
}, async (request, reply) => {
4643
try {
44+
const db = getDb(); // ✅ Called inside route handler when database is ready
45+
const deviceService = new DeviceService(db);
4746
const { deviceId } = request.params as DeviceParams;
4847
const userId = request.user!.id;
4948

services/backend/src/routes/users/me/devices/get.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ import {
1212
} from './schemas';
1313

1414
export default async function getDeviceRoute(server: FastifyInstance) {
15-
const db = getDb();
16-
const deviceService = new DeviceService(db);
17-
1815
server.get('/users/me/devices/:deviceId', {
1916
preValidation: requireAuthentication(),
2017
schema: {
@@ -44,6 +41,8 @@ export default async function getDeviceRoute(server: FastifyInstance) {
4441
}
4542
}, async (request, reply) => {
4643
try {
44+
const db = getDb(); // ✅ Called inside route handler when database is ready
45+
const deviceService = new DeviceService(db);
4746
const { deviceId } = request.params as DeviceParams;
4847
const userId = request.user!.id;
4948

services/backend/src/routes/users/me/devices/list.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import {
1010
} from './schemas';
1111

1212
export default async function listDevicesRoute(server: FastifyInstance) {
13-
const db = getDb();
14-
const deviceService = new DeviceService(db);
15-
1613
server.get('/users/me/devices', {
1714
preValidation: requireAuthentication(),
1815
schema: {
@@ -37,6 +34,8 @@ export default async function listDevicesRoute(server: FastifyInstance) {
3734
}
3835
}, async (request, reply) => {
3936
try {
37+
const db = getDb(); // ✅ Called inside route handler when database is ready
38+
const deviceService = new DeviceService(db);
4039
const userId = request.user!.id;
4140
const devices = await deviceService.getDevicesByUser(userId);
4241

services/backend/src/routes/users/me/devices/update.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ import {
1414
} from './schemas';
1515

1616
export default async function updateDeviceRoute(server: FastifyInstance) {
17-
const db = getDb();
18-
const deviceService = new DeviceService(db);
19-
2017
server.put('/users/me/devices/:deviceId', {
2118
preValidation: requireAuthentication(),
2219
schema: {
@@ -59,6 +56,8 @@ export default async function updateDeviceRoute(server: FastifyInstance) {
5956
}
6057
}, async (request, reply) => {
6158
try {
59+
const db = getDb(); // ✅ Called inside route handler when database is ready
60+
const deviceService = new DeviceService(db);
6261
const { deviceId } = request.params as DeviceParams;
6362
const userId = request.user!.id;
6463
const updateData = request.body as UpdateDeviceRequest;

services/backend/tests/unit/db/index.test.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,26 @@ describe('Database Service (db/index.ts)', () => {
353353
});
354354

355355
describe('getDb and getSchema', () => {
356-
it('should throw if not initialized', () => {
357-
expect(() => getDb()).toThrow('Database not initialized. Call initializeDatabase() first.');
358-
expect(() => getSchema()).toThrow('Database schema not generated. Call initializeDatabase() first.');
356+
it('should return safe proxy/schema when not initialized (graceful startup)', () => {
357+
// New behavior: getDb() returns a safe proxy instead of throwing
358+
const db = getDb();
359+
expect(db).toBeDefined();
360+
expect(typeof db).toBe('object');
361+
362+
// New behavior: getSchema() returns a generated schema instead of throwing
363+
const schema = getSchema();
364+
expect(schema).toBeDefined();
365+
expect(typeof schema).toBe('object');
366+
});
367+
368+
it('should throw helpful error when trying to use proxy database operations', () => {
369+
const db = getDb();
370+
371+
// The proxy should throw helpful errors when operations are attempted
372+
expect(() => db.select()).toThrow('Database not available. Please complete the setup process at /setup first.');
373+
expect(() => db.insert()).toThrow('Database not available. Please complete the setup process at /setup first.');
374+
expect(() => db.update()).toThrow('Database not available. Please complete the setup process at /setup first.');
375+
expect(() => db.delete()).toThrow('Database not available. Please complete the setup process at /setup first.');
359376
});
360377

361378
// This test is removed as it has issues with state persistence between test isolation
@@ -375,11 +392,14 @@ describe('Database Service (db/index.ts)', () => {
375392
});
376393

377394
describe('executeDbOperation', () => {
378-
it('should throw when database is not initialized', () => {
379-
const operation = vi.fn();
395+
it('should throw when database operations are attempted via proxy', () => {
396+
const operation = vi.fn((db, schema) => {
397+
// This will trigger the proxy error when trying to use db operations
398+
return db.select();
399+
});
380400

381-
expect(() => executeDbOperation(operation)).toThrow('Database not initialized');
382-
expect(operation).not.toHaveBeenCalled();
401+
expect(() => executeDbOperation(operation)).toThrow('Database not available. Please complete the setup process at /setup first.');
402+
expect(operation).toHaveBeenCalled(); // Operation is called but throws when using db
383403
});
384404

385405
// These tests are removed as they have issues with state persistence between test isolation

0 commit comments

Comments
 (0)