Skip to content

Commit 4ee57dd

Browse files
authored
Merge pull request #22 from Brayden/bwilmoth/export-csv-json
Support CSV and JSON table exports
2 parents ddcd9fc + 34e9fba commit 4ee57dd

File tree

5 files changed

+153
-3
lines changed

5 files changed

+153
-3
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,32 @@ You can request a `database_dump.sql` file that exports your database schema and
222222

223223
<pre>
224224
<code>
225-
curl --location 'https://starbasedb.YOUR-ID-HERE.workers.dev/dump' \
225+
curl --location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/dump' \
226226
--header 'Authorization: Bearer ABC123'
227227
--output database_dump.sql
228228
</code>
229229
</pre>
230230

231+
<h3>JSON Data Export</h3>
232+
<pre>
233+
<code>
234+
curl
235+
--location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/json/users' \
236+
--header 'Authorization: Bearer ABC123'
237+
--output output.json
238+
</code>
239+
</pre>
240+
241+
<h3>CSV Data Export</h3>
242+
<pre>
243+
<code>
244+
curl
245+
--location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/csv/users' \
246+
--header 'Authorization: Bearer ABC123'
247+
--output output.csv
248+
</code>
249+
</pre>
250+
231251
<br />
232252
<h2>Contributing</h2>
233253
<p>We welcome contributions! Please refer to our <a href="./CONTRIBUTING.md">Contribution Guide</a> for more details.</p>

src/export/csv.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getTableData, createExportResponse } from './index';
2+
import { createResponse } from '../utils';
3+
4+
export async function exportTableToCsvRoute(
5+
sql: any,
6+
operationQueue: any,
7+
ctx: any,
8+
processingOperation: { value: boolean },
9+
tableName: string
10+
): Promise<Response> {
11+
try {
12+
const data = await getTableData(sql, operationQueue, ctx, processingOperation, tableName);
13+
14+
if (data === null) {
15+
return createResponse(undefined, `Table '${tableName}' does not exist.`, 404);
16+
}
17+
18+
// Convert the result to CSV
19+
let csvContent = '';
20+
if (data.length > 0) {
21+
// Add headers
22+
csvContent += Object.keys(data[0]).join(',') + '\n';
23+
24+
// Add data rows
25+
data.forEach((row: any) => {
26+
csvContent += Object.values(row).map(value => {
27+
if (typeof value === 'string' && value.includes(',')) {
28+
return `"${value.replace(/"/g, '""')}"`;
29+
}
30+
return value;
31+
}).join(',') + '\n';
32+
});
33+
}
34+
35+
return createExportResponse(csvContent, `${tableName}_export.csv`, 'text/csv');
36+
} catch (error: any) {
37+
console.error('CSV Export Error:', error);
38+
return createResponse(undefined, 'Failed to export table to CSV', 500);
39+
}
40+
}

src/export/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { enqueueOperation, processNextOperation } from '../operation';
2+
3+
export async function getTableData(
4+
sql: any,
5+
operationQueue: any,
6+
ctx: any,
7+
processingOperation: { value: boolean },
8+
tableName: string
9+
): Promise<any[] | null> {
10+
try {
11+
// Verify if the table exists
12+
const tableExistsResult = await enqueueOperation(
13+
[{ sql: `SELECT name FROM sqlite_master WHERE type='table' AND name=?;`, params: [tableName] }],
14+
false,
15+
false,
16+
operationQueue,
17+
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
18+
);
19+
20+
if (tableExistsResult.result.length === 0) {
21+
return null;
22+
}
23+
24+
// Get table data
25+
const dataResult = await enqueueOperation(
26+
[{ sql: `SELECT * FROM ${tableName};` }],
27+
false,
28+
false,
29+
operationQueue,
30+
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
31+
);
32+
33+
return dataResult.result;
34+
} catch (error: any) {
35+
console.error('Table Data Fetch Error:', error);
36+
throw error;
37+
}
38+
}
39+
40+
export function createExportResponse(data: any, fileName: string, contentType: string): Response {
41+
const blob = new Blob([data], { type: contentType });
42+
43+
const headers = new Headers({
44+
'Content-Type': contentType,
45+
'Content-Disposition': `attachment; filename="${fileName}"`,
46+
});
47+
48+
return new Response(blob, { headers });
49+
}
50+

src/export/json.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getTableData, createExportResponse } from './index';
2+
import { createResponse } from '../utils';
3+
4+
export async function exportTableToJsonRoute(
5+
sql: any,
6+
operationQueue: any,
7+
ctx: any,
8+
processingOperation: { value: boolean },
9+
tableName: string
10+
): Promise<Response> {
11+
try {
12+
const data = await getTableData(sql, operationQueue, ctx, processingOperation, tableName);
13+
14+
if (data === null) {
15+
return createResponse(undefined, `Table '${tableName}' does not exist.`, 404);
16+
}
17+
18+
// Convert the result to JSON
19+
const jsonData = JSON.stringify(data, null, 4);
20+
21+
return createExportResponse(jsonData, `${tableName}_export.json`, 'application/json');
22+
} catch (error: any) {
23+
console.error('JSON Export Error:', error);
24+
return createResponse(undefined, 'Failed to export table to JSON', 500);
25+
}
26+
}

src/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { enqueueOperation, OperationQueueItem, processNextOperation } from './op
44
import { LiteREST } from './literest';
55
import handleStudioRequest from "./studio";
66
import { dumpDatabaseRoute } from './export/dump';
7+
import { exportTableToJsonRoute } from './export/json';
8+
import { exportTableToCsvRoute } from './export/csv';
79

810
const DURABLE_OBJECT_ID = 'sql-durable-object';
911

@@ -125,8 +127,20 @@ export class DatabaseDurableObject extends DurableObject {
125127
return this.statusRoute(request);
126128
} else if (url.pathname.startsWith('/rest')) {
127129
return await this.liteREST.handleRequest(request);
128-
} else if (request.method === 'GET' && url.pathname === '/dump') {
130+
} else if (request.method === 'GET' && url.pathname === '/export/dump') {
129131
return dumpDatabaseRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation);
132+
} else if (request.method === 'GET' && url.pathname.startsWith('/export/json/')) {
133+
const tableName = url.pathname.split('/').pop();
134+
if (!tableName) {
135+
return createResponse(undefined, 'Table name is required', 400);
136+
}
137+
return exportTableToJsonRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName);
138+
} else if (request.method === 'GET' && url.pathname.startsWith('/export/csv/')) {
139+
const tableName = url.pathname.split('/').pop();
140+
if (!tableName) {
141+
return createResponse(undefined, 'Table name is required', 400);
142+
}
143+
return exportTableToCsvRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName);
130144
} else {
131145
return createResponse(undefined, 'Unknown operation', 400);
132146
}
@@ -232,4 +246,4 @@ export default {
232246
*/
233247
return await stub.fetch(request);
234248
},
235-
} satisfies ExportedHandler<Env>;
249+
} satisfies ExportedHandler<Env>;

0 commit comments

Comments
 (0)