Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,32 @@ You can request a `database_dump.sql` file that exports your database schema and

<pre>
<code>
curl --location 'https://starbasedb.YOUR-ID-HERE.workers.dev/dump' \
curl --location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/dump' \
--header 'Authorization: Bearer ABC123'
--output database_dump.sql
</code>
</pre>

<h3>JSON Data Export</h3>
<pre>
<code>
curl
--location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/json/users' \
--header 'Authorization: Bearer ABC123'
--output output.json
</code>
</pre>

<h3>CSV Data Export</h3>
<pre>
<code>
curl
--location 'https://starbasedb.YOUR-ID-HERE.workers.dev/export/csv/users' \
--header 'Authorization: Bearer ABC123'
--output output.csv
</code>
</pre>

<br />
<h2>Contributing</h2>
<p>We welcome contributions! Please refer to our <a href="./CONTRIBUTING.md">Contribution Guide</a> for more details.</p>
Expand Down
40 changes: 40 additions & 0 deletions src/export/csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getTableData, createExportResponse } from './index';
import { createResponse } from '../utils';

export async function exportTableToCsvRoute(
sql: any,
operationQueue: any,
ctx: any,
processingOperation: { value: boolean },
tableName: string
): Promise<Response> {
try {
const data = await getTableData(sql, operationQueue, ctx, processingOperation, tableName);

if (data === null) {
return createResponse(undefined, `Table '${tableName}' does not exist.`, 404);
}

// Convert the result to CSV
let csvContent = '';
if (data.length > 0) {
// Add headers
csvContent += Object.keys(data[0]).join(',') + '\n';

// Add data rows
data.forEach((row: any) => {
csvContent += Object.values(row).map(value => {
if (typeof value === 'string' && value.includes(',')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}).join(',') + '\n';
});
}

return createExportResponse(csvContent, `${tableName}_export.csv`, 'text/csv');
} catch (error: any) {
console.error('CSV Export Error:', error);
return createResponse(undefined, 'Failed to export table to CSV', 500);
}
}
50 changes: 50 additions & 0 deletions src/export/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { enqueueOperation, processNextOperation } from '../operation';

export async function getTableData(
sql: any,
operationQueue: any,
ctx: any,
processingOperation: { value: boolean },
tableName: string
): Promise<any[] | null> {
try {
// Verify if the table exists
const tableExistsResult = await enqueueOperation(
[{ sql: `SELECT name FROM sqlite_master WHERE type='table' AND name=?;`, params: [tableName] }],
false,
false,
operationQueue,
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
);

if (tableExistsResult.result.length === 0) {
return null;
}

// Get table data
const dataResult = await enqueueOperation(
[{ sql: `SELECT * FROM ${tableName};` }],
false,
false,
operationQueue,
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
);

return dataResult.result;
} catch (error: any) {
console.error('Table Data Fetch Error:', error);
throw error;
}
}

export function createExportResponse(data: any, fileName: string, contentType: string): Response {
const blob = new Blob([data], { type: contentType });

const headers = new Headers({
'Content-Type': contentType,
'Content-Disposition': `attachment; filename="${fileName}"`,
});

return new Response(blob, { headers });
}

26 changes: 26 additions & 0 deletions src/export/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getTableData, createExportResponse } from './index';
import { createResponse } from '../utils';

export async function exportTableToJsonRoute(
sql: any,
operationQueue: any,
ctx: any,
processingOperation: { value: boolean },
tableName: string
): Promise<Response> {
try {
const data = await getTableData(sql, operationQueue, ctx, processingOperation, tableName);

if (data === null) {
return createResponse(undefined, `Table '${tableName}' does not exist.`, 404);
}

// Convert the result to JSON
const jsonData = JSON.stringify(data, null, 4);

return createExportResponse(jsonData, `${tableName}_export.json`, 'application/json');
} catch (error: any) {
console.error('JSON Export Error:', error);
return createResponse(undefined, 'Failed to export table to JSON', 500);
}
}
18 changes: 16 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { enqueueOperation, OperationQueueItem, processNextOperation } from './op
import { LiteREST } from './literest';
import handleStudioRequest from "./studio";
import { dumpDatabaseRoute } from './export/dump';
import { exportTableToJsonRoute } from './export/json';
import { exportTableToCsvRoute } from './export/csv';

const DURABLE_OBJECT_ID = 'sql-durable-object';

Expand Down Expand Up @@ -125,8 +127,20 @@ export class DatabaseDurableObject extends DurableObject {
return this.statusRoute(request);
} else if (url.pathname.startsWith('/rest')) {
return await this.liteREST.handleRequest(request);
} else if (request.method === 'GET' && url.pathname === '/dump') {
} else if (request.method === 'GET' && url.pathname === '/export/dump') {
return dumpDatabaseRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation);
} else if (request.method === 'GET' && url.pathname.startsWith('/export/json/')) {
const tableName = url.pathname.split('/').pop();
if (!tableName) {
return createResponse(undefined, 'Table name is required', 400);
}
return exportTableToJsonRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName);
} else if (request.method === 'GET' && url.pathname.startsWith('/export/csv/')) {
const tableName = url.pathname.split('/').pop();
if (!tableName) {
return createResponse(undefined, 'Table name is required', 400);
}
return exportTableToCsvRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName);
} else {
return createResponse(undefined, 'Unknown operation', 400);
}
Expand Down Expand Up @@ -232,4 +246,4 @@ export default {
*/
return await stub.fetch(request);
},
} satisfies ExportedHandler<Env>;
} satisfies ExportedHandler<Env>;