@expresscsv/sdk 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@expresscsv/sdk.svg)](https://www.npmjs.com/package/@expresscsv/sdk)
4
4
  ![license](https://img.shields.io/npm/l/@expresscsv/sdk.svg)
5
5
 
6
- A TypeScript SDK for embedding the [ExpressCSV](https://expresscsv.com) CSV import widget into any web application. Define a schema, open the widget, and receive validated, typed data in chunks.
6
+ A TypeScript SDK for embedding the [ExpressCSV](https://expresscsv.com) CSV importer into any web application. Define a schema, open the importer, and receive validated, typed data in chunks.
7
7
 
8
8
  If you want to define schemas in shared or backend code without any frontend dependencies, use [`@expresscsv/schemas`](https://www.npmjs.com/package/@expresscsv/schemas) for the schema definition itself, then pass that schema into `@expresscsv/sdk`.
9
9
 
@@ -35,223 +35,93 @@ const schema = x.row({
35
35
 
36
36
  const importer = new CSVImporter({
37
37
  schema,
38
- publishableKey: "your-publishable-key",
38
+ getSessionToken: async () => fetchSessionToken(),
39
39
  importIdentifier: "user-import",
40
40
  title: "Import Users",
41
41
  });
42
42
 
43
- // Open the widget and process data in chunks
43
+ // Open the importer and process data in chunks
44
44
  importer.open({
45
45
  onData: (chunk, next) => {
46
46
  console.log(`Chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`);
47
+ console.log("Session:", chunk.sessionId);
48
+ console.log("Idempotency key:", chunk.chunkIdempotencyKey);
47
49
  console.log("Records:", chunk.records);
48
50
  // Process your validated, typed records here
49
51
  next();
50
52
  },
51
- onComplete: () => {
52
- console.log("All chunks processed successfully");
53
+ onComplete: ({ sessionId }) => {
54
+ console.log("All chunks processed successfully for", sessionId);
53
55
  },
54
- onError: (error) => {
55
- console.error("Import failed:", error);
56
+ onError: (error, { sessionId }) => {
57
+ console.error("Import failed for", sessionId, error);
56
58
  },
57
59
  });
58
60
  ```
59
61
 
60
62
  If your schema is assembled dynamically at runtime, still use `x.row(...)`. Just note that dynamic schema assembly can widen TypeScript inference, so use it intentionally when you need runtime-driven columns.
61
63
 
62
- Your `publishableKey` is available from the [ExpressCSV dashboard](https://expresscsv.com). Two key types are available: **production** keys for live usage, and **dev/testing** keys that provide unlimited test imports.
64
+ Have your backend keep the ExpressCSV secret key, call the ExpressCSV session-creation endpoint, and return a short-lived importer session token to the browser via `getSessionToken`. The SDK opens the importer immediately, then completes the initial session bootstrap in the background.
63
65
 
64
- ## Webhook Delivery
66
+ ## Delivery
65
67
 
66
- Deliver imported data directly to your backend via webhook instead of (or in addition to) a local callback:
67
-
68
- ```typescript
69
- importer.open({
70
- webhook: {
71
- url: "https://api.example.com/webhooks/csv-import",
72
- method: "POST",
73
- headers: {
74
- Authorization: "Bearer your-api-token",
75
- },
76
- metadata: {
77
- source: "web-app",
78
- userId: "user-123",
79
- },
80
- },
81
- onComplete: () => {
82
- console.log("Webhook delivery initiated");
83
- },
84
- onError: (error) => {
85
- console.error("Delivery error:", error);
86
- },
87
- });
88
- ```
89
-
90
- ### Webhook Payload Structure
91
-
92
- Each chunk is delivered as a JSON `POST` (or whichever method you configured) to your endpoint. The request body has this shape:
93
-
94
- ```typescript
95
- interface WebhookPayload {
96
- /** Imported records for this chunk, matching your schema */
97
- records: Record<string, unknown>[];
98
- /** 0-based index of the current chunk */
99
- chunkIndex: number;
100
- /** Total number of chunks in this delivery */
101
- totalChunks: number;
102
- /** Total number of records across all chunks */
103
- totalRecords: number;
104
- /** Present only if you passed `metadata` in `WebhookConfig` */
105
- metadata?: Record<string, unknown>;
106
- /** Delivery context added by ExpressCSV */
107
- delivery: {
108
- publishableKey: string;
109
- environmentName: string;
110
- environmentType: string;
111
- teamSlug: string;
112
- importIdentifier: string;
113
- deliveryId: string;
114
- timestamp: string; // ISO 8601
115
- };
116
- }
117
- ```
118
-
119
- When you already have a schema, prefer `InferWebhookPayload<typeof schema>` from `@expresscsv/schemas` instead of rewriting this object shape by hand.
120
-
121
- Example payload:
122
-
123
- ```json
124
- {
125
- "records": [
126
- { "name": "Alice Johnson", "email": "alice@example.com", "age": 32 },
127
- { "name": "Bob Smith", "email": "bob@example.com", "age": 45 }
128
- ],
129
- "chunkIndex": 0,
130
- "totalChunks": 3,
131
- "totalRecords": 2500,
132
- "metadata": {
133
- "source": "web-app",
134
- "userId": "user-123"
135
- },
136
- "delivery": {
137
- "publishableKey": "pk_live_abc123",
138
- "environmentName": "Production",
139
- "environmentType": "production",
140
- "teamSlug": "my-team",
141
- "importIdentifier": "user-import",
142
- "deliveryId": "del_abc123",
143
- "timestamp": "2026-03-02T14:30:00.000Z"
144
- }
145
- }
146
- ```
147
-
148
- The request includes a `Content-Type: application/json` header plus any custom `headers` you specified in `WebhookConfig`.
149
-
150
- ### Handling Webhooks on Your Server
151
-
152
- Your endpoint should return a **2xx** status code to acknowledge each chunk. Non-2xx responses trigger the following retry behaviour:
153
-
154
- - **5xx** and **429** responses are retried automatically (up to 5 attempts per chunk).
155
- - **4xx** responses (except 429) are treated as permanent failures and are **not** retried.
156
-
157
- Chunks are delivered **serially** — the next chunk is only sent after the previous one succeeds.
158
-
159
- If your schema is defined in shared or backend code with `@expresscsv/schemas`, you can reuse the built-in webhook payload helper when handling deliveries:
160
-
161
- ```typescript
162
- import express from "express";
163
- import type { InferWebhookPayload } from "@expresscsv/schemas";
164
- import { employeeSchema } from "../shared/employee-schema";
165
-
166
- const app = express();
167
- app.use(express.json());
168
-
169
- app.post("/webhooks/csv-import", async (req, res) => {
170
- const payload = req.body as InferWebhookPayload<typeof employeeSchema>;
171
-
172
- await db.insertMany("users", payload.records);
173
-
174
- res.status(200).json({ ok: true });
175
- });
176
- ```
177
-
178
- Below is a minimal Express.js example:
179
-
180
- ```typescript
181
- import express from "express";
182
-
183
- const app = express();
184
- app.use(express.json());
185
-
186
- app.post("/webhooks/csv-import", async (req, res) => {
187
- const token = req.headers.authorization;
188
- if (token !== "Bearer your-api-token") {
189
- return res.status(401).json({ error: "Unauthorized" });
190
- }
191
-
192
- const { records, chunkIndex, totalChunks, totalRecords, metadata, delivery } =
193
- req.body;
194
-
195
- try {
196
- // Process the records — e.g. insert into your database
197
- await db.insertMany("users", records);
198
-
199
- console.log(
200
- `Chunk ${chunkIndex + 1}/${totalChunks} processed ` +
201
- `(${records.length} records, delivery ${delivery.deliveryId})`
202
- );
203
-
204
- res.status(200).json({ success: true });
205
- } catch (error) {
206
- // Return 500 so ExpressCSV retries this chunk
207
- console.error("Failed to process chunk:", error);
208
- res.status(500).json({ error: "Internal server error" });
209
- }
210
- });
211
-
212
- app.listen(3000);
213
- ```
214
-
215
- **Tips:**
216
-
217
- - Use `delivery.deliveryId` and `chunkIndex` to **deduplicate** retried chunks (the same chunk may be delivered more than once on retry).
218
- - Use `chunkIndex` and `totalChunks` to track progress and know when the full import is complete (`chunkIndex === totalChunks - 1` for the last chunk).
219
- - Store `metadata` alongside imported records if you need to correlate the import with a specific user or action in your app.
220
-
221
- ### Combined Local Callback and Webhook
222
-
223
- You can use both `onData` and `webhook` simultaneously:
68
+ ExpressCSV delivers imported data through `onData`. Your app receives validated chunks in the browser and can forward them to your backend using whatever request shape fits your stack.
224
69
 
225
70
  ```typescript
226
71
  importer.open({
227
72
  chunkSize: 500,
228
73
  onData: async (chunk, next) => {
229
- await saveToLocalDatabase(chunk.records);
74
+ const response = await fetch("/api/import-users/chunks", {
75
+ method: "POST",
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ },
79
+ body: JSON.stringify({
80
+ sessionId: chunk.sessionId,
81
+ chunkIdempotencyKey: chunk.chunkIdempotencyKey,
82
+ records: chunk.records,
83
+ currentChunkIndex: chunk.currentChunkIndex,
84
+ totalChunks: chunk.totalChunks,
85
+ totalRecords: chunk.totalRecords,
86
+ }),
87
+ });
88
+
89
+ if (!response.ok) {
90
+ throw new Error("Backend rejected this import chunk");
91
+ }
92
+
230
93
  next();
231
94
  },
232
- webhook: {
233
- url: "https://api.example.com/webhooks/csv-import",
234
- headers: { Authorization: "Bearer your-api-token" },
235
- },
236
- onComplete: () => {
237
- console.log("Local processing and webhook delivery complete");
95
+ onComplete: ({ sessionId }) => {
96
+ console.log("Import complete for", sessionId);
238
97
  },
239
98
  });
240
99
  ```
241
100
 
101
+ Each delivered chunk includes:
102
+
103
+ - `records`
104
+ - `sessionId`
105
+ - `chunkIdempotencyKey`
106
+ - `currentChunkIndex`
107
+ - `totalChunks`
108
+ - `totalRecords`
109
+
110
+ Use `sessionId` and `chunkIdempotencyKey` to stage writes safely and deduplicate retries from your own app logic.
111
+
242
112
  ## Preloading
243
113
 
244
- By default, the SDK preloads the widget in a hidden iframe for instant display when `open()` is called. This provides the best user experience.
114
+ By default, the SDK preloads the importer in a hidden iframe for instant display when `open()` is called. This provides the best user experience while the initial session bootstrap continues in the background.
245
115
 
246
116
  ```typescript
247
117
  // Preload is enabled by default
248
118
  const importer = new CSVImporter({
249
119
  schema,
250
- publishableKey: "your-publishable-key",
120
+ getSessionToken: async () => fetchSessionToken(),
251
121
  importIdentifier: "user-import",
252
122
  });
253
123
 
254
- // Later, the widget appears instantly
124
+ // Later, the importer appears instantly
255
125
  importer.open({ onData: (chunk, next) => { /* ... */ next(); } });
256
126
  ```
257
127
 
@@ -260,7 +130,7 @@ To disable preloading (there will be a brief loading screen instead):
260
130
  ```typescript
261
131
  const importer = new CSVImporter({
262
132
  schema,
263
- publishableKey: "your-publishable-key",
133
+ getSessionToken: async () => fetchSessionToken(),
264
134
  importIdentifier: "user-import",
265
135
  preload: false,
266
136
  });
@@ -284,7 +154,7 @@ const candidateSchema = x.row({
284
154
 
285
155
  const importer = new CSVImporter({
286
156
  schema: candidateSchema,
287
- publishableKey: "your-publishable-key",
157
+ getSessionToken: async () => fetchSessionToken(),
288
158
  importIdentifier: "candidate-import",
289
159
  templateDownload: {
290
160
  source: "generate",
@@ -302,7 +172,7 @@ const importer = new CSVImporter({
302
172
 
303
173
  ## Theming and Styling
304
174
 
305
- Customize the widget's appearance with the `theme`, `colorMode`, `customCSS`, and `fonts` options.
175
+ Customize the importer's appearance with the `theme`, `colorMode`, `customCSS`, and `fonts` options.
306
176
 
307
177
  ### Theme
308
178
 
@@ -340,7 +210,7 @@ const dualTheme: ECSVTheme = {
340
210
 
341
211
  const importer = new CSVImporter({
342
212
  schema,
343
- publishableKey: "your-publishable-key",
213
+ getSessionToken: async () => fetchSessionToken(),
344
214
  importIdentifier: "user-import",
345
215
  theme,
346
216
  });
@@ -384,7 +254,7 @@ Control light/dark mode with `colorMode`:
384
254
  ```typescript
385
255
  const importer = new CSVImporter({
386
256
  schema,
387
- publishableKey: "your-publishable-key",
257
+ getSessionToken: async () => fetchSessionToken(),
388
258
  importIdentifier: "user-import",
389
259
  colorMode: "system", // 'light' | 'dark' | 'system'
390
260
  });
@@ -397,7 +267,7 @@ Inject custom CSS for fine-grained styling overrides.
397
267
  ```typescript
398
268
  const importer = new CSVImporter({
399
269
  schema,
400
- publishableKey: "your-publishable-key",
270
+ getSessionToken: async () => fetchSessionToken(),
401
271
  importIdentifier: "user-import",
402
272
  customCSS: `
403
273
  .ecsv [data-step="upload"] {
@@ -417,7 +287,7 @@ Load custom fonts via the `fonts` option:
417
287
  ```typescript
418
288
  const importer = new CSVImporter({
419
289
  schema,
420
- publishableKey: "your-publishable-key",
290
+ getSessionToken: async () => fetchSessionToken(),
421
291
  importIdentifier: "user-import",
422
292
  fonts: {
423
293
  title: { source: "google", name: "Space Grotesk", weights: [400, 600, 700] },
@@ -434,7 +304,7 @@ const importer = new CSVImporter({
434
304
 
435
305
  The `x` schema builder provides a type-safe, fluent API for defining your CSV structure.
436
306
 
437
- For apps that share schema definitions with backend code or webhook handlers, prefer defining the schema in `@expresscsv/schemas` and importing it into your frontend. That keeps schema authoring free of widget/runtime dependencies.
307
+ For apps that share schema definitions with backend code, prefer defining the schema in `@expresscsv/schemas` and importing it into your frontend. That keeps schema authoring free of importer/runtime dependencies.
438
308
 
439
309
  ### Field Types
440
310
 
@@ -539,7 +409,7 @@ All field types support:
539
409
 
540
410
  | Modifier | Description |
541
411
  |---|---|
542
- | `.label(text)` | User-facing label shown in the widget |
412
+ | `.label(text)` | User-facing label shown in the importer |
543
413
  | `.description(text)` | Help text for the field |
544
414
  | `.example(text)` | Example value shown as placeholder |
545
415
  | `.optional()` | Makes the field optional (default is required) |
@@ -590,7 +460,7 @@ x.string().refine(
590
460
  )
591
461
  ```
592
462
 
593
- **Object-returning validator** — inline `valid`, `message`, and an optional `suggestedFix` the widget can offer the user:
463
+ **Object-returning validator** — inline `valid`, `message`, and an optional `suggestedFix` the importer can offer the user:
594
464
 
595
465
  ```typescript
596
466
  x.string().refine((value) => ({
@@ -657,7 +527,7 @@ x.string().refineBatch((values) => {
657
527
 
658
528
  ### `suggestedFix`
659
529
 
660
- Both `.refine()` and `.refineBatch()` support returning a `suggestedFix` object that the widget offers as a one-click fix for the user:
530
+ Both `.refine()` and `.refineBatch()` support returning a `suggestedFix` object that the importer offers as a one-click fix for the user:
661
531
 
662
532
  ```typescript
663
533
  interface SuggestedFix {
@@ -680,24 +550,27 @@ new CSVImporter(options: SDKOptions)
680
550
  | Option | Type | Required | Default | Description |
681
551
  |---|---|---|---|---|
682
552
  | `schema` | Schema | Yes | - | Schema definition created with `x.row()` |
683
- | `publishableKey` | `string` | Yes | - | Your publishable key from the [dashboard](https://expresscsv.com) |
553
+ | `getSessionToken` | `() => Promise<string>` | Yes | - | Async callback that asks your backend for a short-lived importer session token |
684
554
  | `importIdentifier` | `string` | Yes | - | Unique identifier for this import type |
685
- | `title` | `string` | No | - | Title shown in the widget header |
686
- | `preload` | `boolean` | No | `true` | Preload widget for instant display |
555
+ | `title` | `string` | No | - | Title shown in the importer header |
556
+ | `preload` | `boolean` | No | `true` | Preload the importer for instant display |
687
557
  | `debug` | `boolean` | No | `false` | Enable debug logging |
688
558
  | `theme` | `ECSVTheme` | No | - | Custom theme configuration |
689
559
  | `colorMode` | `ColorModePref` | No | - | Light/dark mode (`'light'`, `'dark'`, or `'system'`) |
690
- | `customCSS` | `string` | No | - | Custom CSS to inject into the widget |
560
+ | `customCSS` | `string` | No | - | Custom CSS to inject into the importer |
691
561
  | `fonts` | `Record<string, ECSVFontSource>` | No | - | Custom font sources |
692
562
  | `stepDisplay` | `'progressBar' \| 'segmented' \| 'numbered'` | No | `'progressBar'` | Step indicator style |
693
563
  | `previewSchemaBeforeUpload` | `boolean` | No | `true` | Show schema preview before upload |
564
+ | `aiColumnMatching` | `boolean` | No | `true` | Enable AI-assisted column matching |
565
+ | `aiTransform` | `boolean` | No | `true` | Enable AI-assisted transform generation |
694
566
  | `templateDownload` | `TemplateDownloadOptions<TSchema>` | No | - | Template download configuration with optional schema-typed example rows |
695
- | `saveSession` | `boolean` | No | - | Persist session state |
567
+ | `storage` | `{ type: "local" } \| { type: "custom", ... }` | No | - | Enable Recovered Sessions with the built-in local backend or a custom adapter implementing `get`, `set`, and `remove` |
696
568
  | `locale` | `DeepPartial<ExpressCSVLocaleInput>` | No | - | Localization overrides |
569
+ | `disableStatusStep` | `boolean` | No | - | Skip the success/error status screen |
697
570
 
698
571
  #### `open(options)`
699
572
 
700
- Opens the widget and begins the import flow. At least one of `onData` or `webhook` must be provided.
573
+ Opens the importer and begins the import flow.
701
574
 
702
575
  ```typescript
703
576
  importer.open(options: OpenOptions): void
@@ -705,21 +578,18 @@ importer.open(options: OpenOptions): void
705
578
 
706
579
  | Option | Type | Required | Description |
707
580
  |---|---|---|---|
708
- | `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | * | Callback for each chunk of records. Call `next()` to continue. |
709
- | `webhook` | `WebhookConfig` | * | Webhook endpoint for server-side delivery |
581
+ | `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk of records. Call `next()` to continue. |
710
582
  | `chunkSize` | `number` | No | Records per chunk (default: 1000) |
711
- | `onComplete` | `() => void` | No | Called when all chunks have been processed |
712
- | `onCancel` | `() => void` | No | Called when the user cancels the import |
713
- | `onError` | `(error: Error) => void` | No | Called when an error occurs |
714
- | `onWidgetOpen` | `() => void` | No | Called when the widget opens |
715
- | `onWidgetClose` | `(reason: string) => void` | No | Called when the widget closes |
583
+ | `onComplete` | `(context: { sessionId: string }) => void` | No | Called when all chunks have been processed |
584
+ | `onCancel` | `(context: { sessionId: string }) => void` | No | Called when the user cancels the import |
585
+ | `onError` | `(error: Error, context: { sessionId: string }) => void` | No | Called when an error occurs |
586
+ | `onImporterOpen` | `() => void` | No | Called when the importer opens |
587
+ | `onImporterClose` | `(reason: 'user_close' \| 'cancel' \| 'complete' \| 'error') => void` | No | Called when the importer closes |
716
588
  | `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the wizard step changes |
717
589
 
718
- \* At least one of `onData` or `webhook` is required.
719
-
720
590
  #### `close(reason?)`
721
591
 
722
- Closes the widget and cleans up resources.
592
+ Closes the importer and cleans up resources.
723
593
 
724
594
  ```typescript
725
595
  await importer.close(reason?: 'user_close' | 'cancel' | 'complete' | 'error'): Promise<void>
@@ -731,18 +601,17 @@ await importer.close(reason?: 'user_close' | 'cancel' | 'complete' | 'error'): P
731
601
 
732
602
  | Method | Returns | Description |
733
603
  |---|---|---|
734
- | `getState()` | `WidgetState` | Current widget state |
735
- | `getIsReady()` | `boolean` | Whether the widget is ready or open |
736
- | `getIsOpen()` | `boolean` | Whether the widget is currently open |
604
+ | `getState()` | `ImporterState` | Current importer lifecycle state |
605
+ | `getIsReady()` | `boolean` | Whether the importer is ready or open |
606
+ | `getIsOpen()` | `boolean` | Whether the importer is currently open |
737
607
  | `getConnectionStatus()` | `boolean` | Whether the iframe connection is active |
738
- | `getCanRestart()` | `boolean` | Whether the widget can be restarted |
608
+ | `getCanRestart()` | `boolean` | Whether the importer can be restarted |
739
609
  | `getLastError()` | `Error \| null` | Last error, if any |
740
610
  | `getStatus()` | `object` | Comprehensive status snapshot |
741
- | `getVersion()` | `string` | SDK version |
742
611
 
743
612
  #### `restart(newOptions?)`
744
613
 
745
- Restarts the widget, optionally with updated options. Returns `Promise<void>`.
614
+ Restarts the importer, optionally with updated options. Returns `Promise<void>`.
746
615
 
747
616
  ### `RecordsChunk<T>`
748
617
 
@@ -754,19 +623,8 @@ interface RecordsChunk<T> {
754
623
  totalChunks: number;
755
624
  currentChunkIndex: number;
756
625
  totalRecords: number;
757
- }
758
- ```
759
-
760
- ### `WebhookConfig`
761
-
762
- ```typescript
763
- interface WebhookConfig {
764
- url: string;
765
- headers?: Record<string, string>;
766
- method?: "POST" | "PUT" | "PATCH";
767
- timeout?: number;
768
- retries?: number;
769
- metadata?: Record<string, unknown>;
626
+ sessionId: string;
627
+ chunkIdempotencyKey: string;
770
628
  }
771
629
  ```
772
630
 
@@ -788,7 +646,7 @@ type Row = Infer<typeof schema>;
788
646
 
789
647
  const importer = new CSVImporter({
790
648
  schema,
791
- publishableKey: "your-publishable-key",
649
+ getSessionToken: async () => fetchSessionToken(),
792
650
  importIdentifier: "user-import",
793
651
  });
794
652