Skip to content

Commit 0e62e8d

Browse files
authored
Merge pull request #11 from StuMason/feature/database-management
Feature/database management
2 parents 60f77e4 + 6def2fd commit 0e62e8d

File tree

6 files changed

+567
-22
lines changed

6 files changed

+567
-22
lines changed

docs/features/006-database-management.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,28 @@ Implementation of database management features through MCP resources, allowing u
4242

4343
## Implementation Checklist
4444

45-
- [ ] Basic Database Management
46-
47-
- [ ] List databases resource
48-
- [ ] Get database details
49-
- [ ] Delete database
50-
- [ ] Update database configuration
51-
52-
- [ ] Database Type Support
53-
54-
- [ ] PostgreSQL configuration
55-
- [ ] MariaDB configuration
56-
- [ ] MySQL configuration
57-
- [ ] MongoDB configuration
58-
- [ ] Redis configuration
59-
- [ ] KeyDB configuration
60-
- [ ] Clickhouse configuration
61-
- [ ] Dragonfly configuration
62-
63-
- [ ] Resource Testing
64-
- [ ] Unit tests for database operations
65-
- [ ] Integration tests with mock data
66-
- [ ] Live test with real Coolify instance
45+
- [x] Basic Database Management
46+
47+
- [x] List databases resource
48+
- [x] Get database details
49+
- [x] Delete database
50+
- [x] Update database configuration
51+
52+
- [x] Database Type Support
53+
54+
- [x] PostgreSQL configuration
55+
- [x] MariaDB configuration
56+
- [x] MySQL configuration
57+
- [x] MongoDB configuration
58+
- [x] Redis configuration
59+
- [x] KeyDB configuration
60+
- [x] Clickhouse configuration
61+
- [x] Dragonfly configuration
62+
63+
- [x] Resource Testing
64+
- [x] Unit tests for database operations
65+
- [x] Integration tests with mock data
66+
- [x] Live test with real Coolify instance
6767

6868
## Dependencies
6969

src/__tests__/coolify-client.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,124 @@ describe('CoolifyClient', () => {
341341
);
342342
});
343343
});
344+
345+
describe('Database Management', () => {
346+
const mockDatabase = {
347+
id: 1,
348+
uuid: 'test-db-uuid',
349+
name: 'test-db',
350+
description: 'Test database',
351+
type: 'postgresql' as const,
352+
status: 'running' as const,
353+
created_at: '2024-03-06T12:00:00Z',
354+
updated_at: '2024-03-06T12:00:00Z',
355+
is_public: false,
356+
image: 'postgres:latest',
357+
postgres_user: 'postgres',
358+
postgres_password: 'test123',
359+
postgres_db: 'testdb',
360+
};
361+
362+
beforeEach(() => {
363+
mockFetch.mockClear();
364+
});
365+
366+
it('should list databases', async () => {
367+
mockFetch.mockResolvedValueOnce({
368+
ok: true,
369+
json: () => Promise.resolve([mockDatabase]),
370+
});
371+
372+
const result = await client.listDatabases();
373+
374+
expect(result).toEqual([mockDatabase]);
375+
expect(mockFetch).toHaveBeenCalledWith(
376+
'http://test.coolify.io/api/v1/databases',
377+
expect.objectContaining({
378+
headers: {
379+
'Content-Type': 'application/json',
380+
Authorization: 'Bearer test-token',
381+
},
382+
}),
383+
);
384+
});
385+
386+
it('should get database details', async () => {
387+
mockFetch.mockResolvedValueOnce({
388+
ok: true,
389+
json: () => Promise.resolve(mockDatabase),
390+
});
391+
392+
const result = await client.getDatabase('test-db-uuid');
393+
394+
expect(result).toEqual(mockDatabase);
395+
expect(mockFetch).toHaveBeenCalledWith(
396+
'http://test.coolify.io/api/v1/databases/test-db-uuid',
397+
expect.objectContaining({
398+
headers: {
399+
'Content-Type': 'application/json',
400+
Authorization: 'Bearer test-token',
401+
},
402+
}),
403+
);
404+
});
405+
406+
it('should update database', async () => {
407+
const updateData = {
408+
name: 'updated-db',
409+
description: 'Updated description',
410+
};
411+
mockFetch.mockResolvedValueOnce({
412+
ok: true,
413+
json: () => Promise.resolve({ ...mockDatabase, ...updateData }),
414+
});
415+
416+
const result = await client.updateDatabase('test-db-uuid', updateData);
417+
418+
expect(result).toEqual({ ...mockDatabase, ...updateData });
419+
expect(mockFetch).toHaveBeenCalledWith(
420+
'http://test.coolify.io/api/v1/databases/test-db-uuid',
421+
expect.objectContaining({
422+
method: 'PATCH',
423+
body: JSON.stringify(updateData),
424+
headers: {
425+
'Content-Type': 'application/json',
426+
Authorization: 'Bearer test-token',
427+
},
428+
}),
429+
);
430+
});
431+
432+
it('should delete database', async () => {
433+
const mockResponse = { message: 'Database deleted' };
434+
mockFetch.mockResolvedValueOnce({
435+
ok: true,
436+
json: () => Promise.resolve(mockResponse),
437+
});
438+
439+
const result = await client.deleteDatabase('test-db-uuid', {
440+
deleteConfigurations: true,
441+
deleteVolumes: true,
442+
});
443+
444+
expect(result).toEqual(mockResponse);
445+
expect(mockFetch).toHaveBeenCalledWith(
446+
'http://test.coolify.io/api/v1/databases/test-db-uuid?delete_configurations=true&delete_volumes=true',
447+
expect.objectContaining({
448+
method: 'DELETE',
449+
headers: {
450+
'Content-Type': 'application/json',
451+
Authorization: 'Bearer test-token',
452+
},
453+
}),
454+
);
455+
});
456+
457+
it('should handle database errors', async () => {
458+
const errorMessage = 'Database not found';
459+
mockFetch.mockRejectedValue(new Error(errorMessage));
460+
461+
await expect(client.getDatabase('invalid-uuid')).rejects.toThrow(errorMessage);
462+
});
463+
});
344464
});

src/__tests__/mcp-server.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,82 @@ describe('CoolifyMcpServer', () => {
193193
);
194194
});
195195
});
196+
197+
describe('Database Management', () => {
198+
const mockDatabase = {
199+
id: 1,
200+
uuid: 'test-db-uuid',
201+
name: 'test-db',
202+
description: 'Test database',
203+
type: 'postgresql' as const,
204+
status: 'running' as const,
205+
created_at: '2024-03-06T12:00:00Z',
206+
updated_at: '2024-03-06T12:00:00Z',
207+
is_public: false,
208+
image: 'postgres:latest',
209+
postgres_user: 'postgres',
210+
postgres_password: 'test123',
211+
postgres_db: 'testdb',
212+
};
213+
214+
it('should list databases', async () => {
215+
const spy = jest.spyOn(server['client'], 'listDatabases').mockResolvedValue([mockDatabase]);
216+
217+
const result = await server.list_databases();
218+
219+
expect(result).toEqual([mockDatabase]);
220+
expect(spy).toHaveBeenCalled();
221+
});
222+
223+
it('should get database details', async () => {
224+
const spy = jest.spyOn(server['client'], 'getDatabase').mockResolvedValue(mockDatabase);
225+
226+
const result = await server.get_database('test-db-uuid');
227+
228+
expect(result).toEqual(mockDatabase);
229+
expect(spy).toHaveBeenCalledWith('test-db-uuid');
230+
});
231+
232+
it('should update database', async () => {
233+
const updateData = {
234+
name: 'updated-db',
235+
description: 'Updated description',
236+
};
237+
const spy = jest
238+
.spyOn(server['client'], 'updateDatabase')
239+
.mockResolvedValue({ ...mockDatabase, ...updateData, type: 'postgresql' as const });
240+
241+
const result = await server.update_database('test-db-uuid', updateData);
242+
243+
expect(result).toEqual({
244+
...mockDatabase,
245+
...updateData,
246+
type: 'postgresql',
247+
});
248+
expect(spy).toHaveBeenCalledWith('test-db-uuid', updateData);
249+
});
250+
251+
it('should delete database', async () => {
252+
const mockResponse = { message: 'Database deleted' };
253+
const spy = jest.spyOn(server['client'], 'deleteDatabase').mockResolvedValue(mockResponse);
254+
255+
const result = await server.delete_database('test-db-uuid', {
256+
deleteConfigurations: true,
257+
deleteVolumes: true,
258+
});
259+
260+
expect(result).toEqual(mockResponse);
261+
expect(spy).toHaveBeenCalledWith('test-db-uuid', {
262+
deleteConfigurations: true,
263+
deleteVolumes: true,
264+
});
265+
});
266+
267+
it('should handle database errors', async () => {
268+
const errorMessage = 'Database not found';
269+
jest.spyOn(server['client'], 'getDatabase').mockRejectedValue(new Error(errorMessage));
270+
271+
await expect(server.get_database('invalid-uuid')).rejects.toThrow(errorMessage);
272+
});
273+
});
196274
});

src/lib/coolify-client.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
UpdateProjectRequest,
1111
Environment,
1212
Deployment,
13+
Database,
14+
DatabaseUpdateRequest,
1315
} from '../types/coolify.js';
1416

1517
export class CoolifyClient {
@@ -128,5 +130,53 @@ export class CoolifyClient {
128130
return response;
129131
}
130132

133+
async listDatabases(): Promise<Database[]> {
134+
return this.request<Database[]>('/databases');
135+
}
136+
137+
async getDatabase(uuid: string): Promise<Database> {
138+
return this.request<Database>(`/databases/${uuid}`);
139+
}
140+
141+
async updateDatabase(uuid: string, data: DatabaseUpdateRequest): Promise<Database> {
142+
return this.request<Database>(`/databases/${uuid}`, {
143+
method: 'PATCH',
144+
body: JSON.stringify(data),
145+
});
146+
}
147+
148+
async deleteDatabase(
149+
uuid: string,
150+
options?: {
151+
deleteConfigurations?: boolean;
152+
deleteVolumes?: boolean;
153+
dockerCleanup?: boolean;
154+
deleteConnectedNetworks?: boolean;
155+
},
156+
): Promise<{ message: string }> {
157+
const queryParams = new URLSearchParams();
158+
if (options) {
159+
if (options.deleteConfigurations !== undefined) {
160+
queryParams.set('delete_configurations', options.deleteConfigurations.toString());
161+
}
162+
if (options.deleteVolumes !== undefined) {
163+
queryParams.set('delete_volumes', options.deleteVolumes.toString());
164+
}
165+
if (options.dockerCleanup !== undefined) {
166+
queryParams.set('docker_cleanup', options.dockerCleanup.toString());
167+
}
168+
if (options.deleteConnectedNetworks !== undefined) {
169+
queryParams.set('delete_connected_networks', options.deleteConnectedNetworks.toString());
170+
}
171+
}
172+
173+
const queryString = queryParams.toString();
174+
const url = queryString ? `/databases/${uuid}?${queryString}` : `/databases/${uuid}`;
175+
176+
return this.request<{ message: string }>(url, {
177+
method: 'DELETE',
178+
});
179+
}
180+
131181
// Add more methods as needed for other endpoints
132182
}

0 commit comments

Comments
 (0)