Skip to content

Commit e447d4c

Browse files
committed
Support CSV data import
1 parent a49849e commit e447d4c

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

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

1213
const DURABLE_OBJECT_ID = 'sql-durable-object';
1314

@@ -151,6 +152,12 @@ export class DatabaseDurableObject extends DurableObject {
151152
return createResponse(undefined, 'Table name is required', 400);
152153
}
153154
return importTableFromJsonRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName, request);
155+
} else if (request.method === 'POST' && url.pathname.startsWith('/import/csv/')) {
156+
const tableName = url.pathname.split('/').pop();
157+
if (!tableName) {
158+
return createResponse(undefined, 'Table name is required', 400);
159+
}
160+
return importTableFromCsvRoute(this.sql, this.operationQueue, this.ctx, this.processingOperation, tableName, request);
154161
} else {
155162
return createResponse(undefined, 'Unknown operation', 400);
156163
}

0 commit comments

Comments
 (0)