Skip to content

Commit 62cb886

Browse files
authored
Merge branch 'main' into bwilmoth/template-auth
2 parents 60bfc45 + 1f3cbe3 commit 62cb886

File tree

3 files changed

+146
-9
lines changed

3 files changed

+146
-9
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<div align="center">
1313
<a href="https://github.com/Brayden/starbasedb/releases"><img src="https://img.shields.io/github/v/release/Brayden/starbasedb?display_name=tag&style=flat"></img></a>
14+
<a href="https://starbasedb.hashnode.space/default-guide/introduction/quick-start"><img src="https://img.shields.io/static/v1?label=Read&message=Documentation&color=purple&style=flat"></img></a>
1415
<a href="https://starbasedb.com"><img src="https://img.shields.io/static/v1?label=Website&message=StarbaseDB&color=%23be185d&style=flat"></img></a>
1516
<a href="https://twitter.com/BraydenWilmoth"><img src="https://img.shields.io/static/v1?label=Follow&message=@BraydenWilmoth&color=black&style=flat"></img></a>
1617
<a href="https://discord.gg/k2J7jcJCvd"><img src="https://img.shields.io/static/v1?label=Join us on&message=Discord&color=%237289da&style=flat"></img></a>
@@ -20,13 +21,12 @@
2021
<br />
2122
<h2>Features</h2>
2223
<ul>
23-
<li><strong><a href="https://github.com/Brayden/starbasedb/edit/main/README.md#executing-queries">HTTPS Endpoints</a></strong> to query & interact with your database</li>
24-
<li><strong><a href="https://github.com/Brayden/starbasedb?tab=readme-ov-file#web-sockets">Web Socket Connections</a></strong> to query your database with low-latency web sockets</li>
25-
<li><strong><a href="https://github.com/Brayden/starbasedb?tab=readme-ov-file#transactions">Transactions Support</a></strong> for executing interdependent collections of queries</li>
26-
<li><strong><a href="https://github.com/Brayden/starbasedb/blob/main/src/literest/README.md">REST API Support</a></strong> automatically included for interacting with your tables</li>
24+
<li><strong><a href="https://starbasedb.hashnode.space/default-guide/http-endpoints/query">HTTPS Endpoints</a></strong> to query & interact with your database</li>
25+
<li><strong><a href="https://starbasedb.hashnode.space/default-guide/web-sockets/introduction">Web Socket Connections</a></strong> to query your database with low-latency web sockets</li>
26+
<li><strong><a href="https://starbasedb.hashnode.space/default-guide/http-endpoints/transactions">Transactions Support</a></strong> for executing interdependent collections of queries</li>
27+
<li><strong><a href="https://starbasedb.hashnode.space/default-guide/rest-api/introduction">REST API Support</a></strong> automatically included for interacting with your tables</li>
2728
<li><strong><a href="https://github.com/Brayden/starbasedb/edit/main/README.md#deploy-a-starbasedb">Database Interface</a></strong> included out of the box deployed with your Cloudflare Worker</li>
28-
<li><strong><a href="https://github.com/Brayden/starbasedb?tab=readme-ov-file#sql-dump">Export SQL Dump</a></strong> to extract your schema and data into a local `.sql` file</li>
29-
<li><strong><a href="https://github.com/Brayden/starbasedb?tab=readme-ov-file#sql-import">Import SQL Dump</a></strong> to import your schema and data from a local `.sql` file</li>
29+
<li><strong><a href="https://starbasedb.hashnode.space/default-guide/import-export/sql-dump">Import & Export Data</a></strong> to import & extract your schema and data into a local `.sql`, `.json` or `.csv` file</li>
3030
<li><strong>Scale-to-zero Compute</strong> to reduce costs when your database is not in use</li>
3131
</ul>
3232
<br />
@@ -39,8 +39,8 @@
3939
<li><strong>Data Replication</strong> to scale reads beyond the 1,000 RPS limitation</li>
4040
<li><strong>Data Streaming</strong> for streaming responses back as rows are read</li>
4141
<li><strong>Data Syncing</strong> between local source and your database</li>
42-
<li><strong>Export Data</strong> as a CSV, JSON or SQLite file</li>
43-
<li><strong>Import Data</strong> from a CSV, JSON or SQLite file</li>
42+
<li><strong>Service Templates</strong> for out of the box features such as user authentication, analytics and more</li>
43+
<li><strong>Scheduled CRON Tasks</strong> to execute code at desired intervals</li>
4444
</ul>
4545

4646
<br />
@@ -266,7 +266,7 @@ curl
266266

267267
<br />
268268
<h2>License</h2>
269-
<p>This project is licensed under the MIT license. See the <a href="./LICENSE">LICENSE</a> file for more info.</p>
269+
<p>This project is licensed under the AGPL-3.0 license. See the <a href="./LICENSE">LICENSE</a> file for more info.</p>
270270

271271
<br />
272272
<h2>Contributors</h2>

src/import/csv.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { createResponse } from '../utils';
2+
import { enqueueOperation, processNextOperation } from '../operation';
3+
4+
interface ColumnMapping {
5+
[key: string]: string;
6+
}
7+
8+
interface CsvData {
9+
data: string;
10+
columnMapping?: Record<string, string>;
11+
}
12+
13+
export async function importTableFromCsvRoute(
14+
sql: SqlStorage,
15+
operationQueue: any,
16+
ctx: any,
17+
processingOperation: { value: boolean },
18+
tableName: string,
19+
request: Request
20+
): Promise<Response> {
21+
try {
22+
if (!request.body) {
23+
return createResponse(undefined, 'Request body is empty', 400);
24+
}
25+
26+
let csvData: CsvData;
27+
const contentType = request.headers.get('Content-Type') || '';
28+
29+
if (contentType.includes('application/json')) {
30+
// Handle JSON-wrapped CSV data in POST body
31+
csvData = await request.json() as CsvData;
32+
} else if (contentType.includes('text/csv')) {
33+
// Handle raw CSV data in POST body
34+
const csvContent = await request.text();
35+
csvData = { data: csvContent };
36+
} else if (contentType.includes('multipart/form-data')) {
37+
// Handle file upload
38+
const formData = await request.formData();
39+
const file = formData.get('file') as File | null;
40+
41+
if (!file) {
42+
return createResponse(undefined, 'No file uploaded', 400);
43+
}
44+
45+
const csvContent = await file.text();
46+
csvData = { data: csvContent };
47+
} else {
48+
return createResponse(undefined, 'Unsupported Content-Type', 400);
49+
}
50+
51+
const { data: csvContent, columnMapping = {} } = csvData;
52+
53+
// Parse CSV data
54+
const records = parseCSV(csvContent);
55+
56+
if (records.length === 0) {
57+
return createResponse(undefined, 'Invalid CSV format or empty data', 400);
58+
}
59+
60+
const failedStatements: { statement: string; error: string }[] = [];
61+
let successCount = 0;
62+
63+
for (const record of records) {
64+
const mappedRecord = mapRecord(record, columnMapping);
65+
const columns = Object.keys(mappedRecord);
66+
const values = Object.values(mappedRecord);
67+
const placeholders = values.map(() => '?').join(', ');
68+
69+
const statement = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
70+
71+
try {
72+
await enqueueOperation(
73+
[{ sql: statement, params: values }],
74+
false,
75+
false,
76+
operationQueue,
77+
() => processNextOperation(sql, operationQueue, ctx, processingOperation)
78+
);
79+
successCount++;
80+
} catch (error: any) {
81+
failedStatements.push({
82+
statement: statement,
83+
error: error.message || 'Unknown error'
84+
});
85+
}
86+
}
87+
88+
const totalRecords = records.length;
89+
const failedCount = failedStatements.length;
90+
91+
const resultMessage = `Imported ${successCount} out of ${totalRecords} records successfully. ${failedCount} records failed.`;
92+
93+
return createResponse({
94+
message: resultMessage,
95+
failedStatements: failedStatements
96+
}, undefined, 200);
97+
98+
} catch (error: any) {
99+
console.error('CSV Import Error:', error);
100+
return createResponse(undefined, 'Failed to import CSV data: ' + error.message, 500);
101+
}
102+
}
103+
104+
function parseCSV(csv: string): Record<string, string>[] {
105+
const lines = csv.split('\n');
106+
const headers = lines[0].split(',').map(header => header.trim());
107+
const records: Record<string, string>[] = [];
108+
109+
for (let i = 1; i < lines.length; i++) {
110+
const values = lines[i].split(',').map(value => value.trim());
111+
if (values.length === headers.length) {
112+
const record: Record<string, string> = {};
113+
headers.forEach((header, index) => {
114+
record[header] = values[index];
115+
});
116+
records.push(record);
117+
}
118+
}
119+
120+
return records;
121+
}
122+
123+
function mapRecord(record: any, columnMapping: ColumnMapping): any {
124+
const mappedRecord: any = {};
125+
for (const [key, value] of Object.entries(record)) {
126+
const mappedKey = columnMapping[key] || key;
127+
mappedRecord[mappedKey] = value;
128+
}
129+
return mappedRecord;
130+
}

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { exportTableToJsonRoute } from './export/json';
88
import { exportTableToCsvRoute } from './export/csv';
99
import { importDumpRoute } from './import/dump';
1010
import { importTableFromJsonRoute } from './import/json';
11+
import { importTableFromCsvRoute } from './import/csv';
1112
import { handleApiRequest } from "./api";
1213

1314
const DURABLE_OBJECT_ID = 'sql-durable-object';
@@ -188,6 +189,12 @@ export class DatabaseDurableObject extends DurableObject {
188189
return createResponse(undefined, 'Table name is required', 400);
189190
}
190191
return importTableFromJsonRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName, request);
192+
} else if (request.method === 'POST' && url.pathname.startsWith('/import/csv/')) {
193+
const tableName = url.pathname.split('/').pop();
194+
if (!tableName) {
195+
return createResponse(undefined, 'Table name is required', 400);
196+
}
197+
return importTableFromCsvRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName, request);
191198
} else if (url.pathname.startsWith('/api')) {
192199
return await handleApiRequest(request);
193200
} else {

0 commit comments

Comments
 (0)