@expresscsv/sdk 0.1.25 → 1.0.0

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
@@ -36,21 +36,20 @@ const schema = x.row({
36
36
  const importer = new CSVImporter({
37
37
  schema,
38
38
  getSessionToken: async () => fetchSessionToken(),
39
- importIdentifier: "user-import",
39
+ importNamespace: "user-import",
40
40
  title: "Import Users",
41
41
  });
42
42
 
43
43
  // Open the importer and process data in chunks
44
44
  importer.open({
45
45
  onData: (chunk, next) => {
46
- console.log(`Chunk ${chunk.currentChunkIndex + 1}/${chunk.totalChunks}`);
46
+ console.log(`Chunk ${chunk.chunkIndex + 1}/${chunk.totalChunks}`);
47
47
  console.log("Session:", chunk.sessionId);
48
- console.log("Idempotency key:", chunk.chunkIdempotencyKey);
49
48
  console.log("Records:", chunk.records);
50
49
  // Process your validated, typed records here
51
50
  next();
52
51
  },
53
- onComplete: ({ sessionId }) => {
52
+ onComplete: ({ sessionId, deliveryId }) => {
54
53
  console.log("All chunks processed successfully for", sessionId);
55
54
  },
56
55
  onError: (error, { sessionId }) => {
@@ -69,18 +68,19 @@ ExpressCSV delivers imported data through `onData`. Your app receives validated
69
68
 
70
69
  ```typescript
71
70
  importer.open({
72
- chunkSize: 500,
71
+ chunkSize: { unit: "kb", value: 500 },
73
72
  onData: async (chunk, next) => {
74
- const response = await fetch("/api/import-users/chunks", {
73
+ const response = await fetch("/your-api/import-users/chunks", {
75
74
  method: "POST",
76
75
  headers: {
77
76
  "Content-Type": "application/json",
77
+ Authorization: `Bearer ${accessToken}`,
78
78
  },
79
79
  body: JSON.stringify({
80
80
  sessionId: chunk.sessionId,
81
- chunkIdempotencyKey: chunk.chunkIdempotencyKey,
81
+ deliveryId: chunk.deliveryId,
82
+ chunkIndex: chunk.chunkIndex,
82
83
  records: chunk.records,
83
- currentChunkIndex: chunk.currentChunkIndex,
84
84
  totalChunks: chunk.totalChunks,
85
85
  totalRecords: chunk.totalRecords,
86
86
  }),
@@ -92,8 +92,15 @@ importer.open({
92
92
 
93
93
  next();
94
94
  },
95
- onComplete: ({ sessionId }) => {
96
- console.log("Import complete for", sessionId);
95
+ onComplete: async ({ sessionId, deliveryId }) => {
96
+ await fetch("/your-api/import-users/complete", {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/json",
100
+ Authorization: `Bearer ${accessToken}`,
101
+ },
102
+ body: JSON.stringify({ sessionId, deliveryId }),
103
+ });
97
104
  },
98
105
  });
99
106
  ```
@@ -102,12 +109,13 @@ Each delivered chunk includes:
102
109
 
103
110
  - `records`
104
111
  - `sessionId`
105
- - `chunkIdempotencyKey`
106
- - `currentChunkIndex`
112
+ - `deliveryId`
113
+ - `chunkIndex`
107
114
  - `totalChunks`
108
115
  - `totalRecords`
109
116
 
110
- Use `sessionId` and `chunkIdempotencyKey` to stage writes safely and deduplicate retries from your own app logic.
117
+ Each time the user finishes the import, ExpressCSV creates a new `deliveryId` for that `sessionId`. Your `onData` code can fail, and the user can retry delivery without starting the import over. Store chunks by `(sessionId, deliveryId, chunkIndex)`, then finalize the `deliveryId` from `onComplete` after all of its chunks are accepted.
118
+
111
119
 
112
120
  ## Preloading
113
121
 
@@ -118,7 +126,7 @@ By default, the SDK preloads the importer in a hidden iframe for instant display
118
126
  const importer = new CSVImporter({
119
127
  schema,
120
128
  getSessionToken: async () => fetchSessionToken(),
121
- importIdentifier: "user-import",
129
+ importNamespace: "user-import",
122
130
  });
123
131
 
124
132
  // Later, the importer appears instantly
@@ -131,7 +139,7 @@ To disable preloading (there will be a brief loading screen instead):
131
139
  const importer = new CSVImporter({
132
140
  schema,
133
141
  getSessionToken: async () => fetchSessionToken(),
134
- importIdentifier: "user-import",
142
+ importNamespace: "user-import",
135
143
  preload: false,
136
144
  });
137
145
  ```
@@ -155,7 +163,7 @@ const candidateSchema = x.row({
155
163
  const importer = new CSVImporter({
156
164
  schema: candidateSchema,
157
165
  getSessionToken: async () => fetchSessionToken(),
158
- importIdentifier: "candidate-import",
166
+ importNamespace: "candidate-import",
159
167
  templateDownload: {
160
168
  source: "generate",
161
169
  formats: ["csv", "xlsx"],
@@ -179,10 +187,10 @@ Customize the importer's appearance with the `theme`, `colorMode`, `customCSS`,
179
187
  Use the `theme` option to override CSS variables (colors, radius, typography). Pass either a single theme (applies to both light and dark) or a dual-mode theme with separate light/dark values:
180
188
 
181
189
  ```typescript
182
- import { CSVImporter, x, type ECSVTheme } from "@expresscsv/sdk";
190
+ import { CSVImporter, x, type Theme } from "@expresscsv/sdk";
183
191
 
184
192
  // Single theme (both modes)
185
- const theme: ECSVTheme = {
193
+ const theme: Theme = {
186
194
  primary: "#4F46E5",
187
195
  "primary-foreground": "#ffffff",
188
196
  background: "#ffffff",
@@ -193,7 +201,7 @@ const theme: ECSVTheme = {
193
201
  };
194
202
 
195
203
  // Dual-mode (light and dark)
196
- const dualTheme: ECSVTheme = {
204
+ const dualTheme: Theme = {
197
205
  modes: {
198
206
  light: {
199
207
  primary: "#4F46E5",
@@ -211,7 +219,7 @@ const dualTheme: ECSVTheme = {
211
219
  const importer = new CSVImporter({
212
220
  schema,
213
221
  getSessionToken: async () => fetchSessionToken(),
214
- importIdentifier: "user-import",
222
+ importNamespace: "user-import",
215
223
  theme,
216
224
  });
217
225
  ```
@@ -255,7 +263,7 @@ Control light/dark mode with `colorMode`:
255
263
  const importer = new CSVImporter({
256
264
  schema,
257
265
  getSessionToken: async () => fetchSessionToken(),
258
- importIdentifier: "user-import",
266
+ importNamespace: "user-import",
259
267
  colorMode: "system", // 'light' | 'dark' | 'system'
260
268
  });
261
269
  ```
@@ -268,7 +276,7 @@ Inject custom CSS for fine-grained styling overrides.
268
276
  const importer = new CSVImporter({
269
277
  schema,
270
278
  getSessionToken: async () => fetchSessionToken(),
271
- importIdentifier: "user-import",
279
+ importNamespace: "user-import",
272
280
  customCSS: `
273
281
  .ecsv [data-step="upload"] {
274
282
  border-radius: 1rem;
@@ -288,7 +296,7 @@ Load custom fonts via the `fonts` option:
288
296
  const importer = new CSVImporter({
289
297
  schema,
290
298
  getSessionToken: async () => fetchSessionToken(),
291
- importIdentifier: "user-import",
299
+ importNamespace: "user-import",
292
300
  fonts: {
293
301
  title: { source: "google", name: "Space Grotesk", weights: [400, 600, 700] },
294
302
  body: { source: "custom", url: "https://example.com/font.woff2", format: "woff2" },
@@ -411,7 +419,6 @@ All field types support:
411
419
  |---|---|
412
420
  | `.label(text)` | User-facing label shown in the importer |
413
421
  | `.description(text)` | Help text for the field |
414
- | `.example(text)` | Example value shown as placeholder |
415
422
  | `.optional()` | Makes the field optional (default is required) |
416
423
  | `.refine(fn)` | Custom validation function |
417
424
 
@@ -551,26 +558,26 @@ new CSVImporter(options: SDKOptions)
551
558
  |---|---|---|---|---|
552
559
  | `schema` | Schema | Yes | - | Schema definition created with `x.row()` |
553
560
  | `getSessionToken` | `() => Promise<string>` | Yes | - | Async callback that asks your backend for a short-lived importer session token |
554
- | `importIdentifier` | `string` | Yes | - | Unique identifier for this import type |
561
+ | `importNamespace` | `string` | Yes | - | Stable namespace string your app assigns to this importer configuration. Keep it the same for the same workflow; use a different value for different importers. |
555
562
  | `title` | `string` | No | - | Title shown in the importer header |
556
563
  | `preload` | `boolean` | No | `true` | Preload the importer for instant display |
557
564
  | `debug` | `boolean` | No | `false` | Enable debug logging |
558
- | `theme` | `ECSVTheme` | No | - | Custom theme configuration |
565
+ | `theme` | `Theme` | No | - | Custom theme configuration |
559
566
  | `colorMode` | `ColorModePref` | No | - | Light/dark mode (`'light'`, `'dark'`, or `'system'`) |
560
567
  | `customCSS` | `string` | No | - | Custom CSS to inject into the importer |
561
- | `fonts` | `Record<string, ECSVFontSource>` | No | - | Custom font sources |
568
+ | `fonts` | `Record<string, FontSource>` | No | - | Custom font sources |
562
569
  | `stepDisplay` | `'progressBar' \| 'segmented' \| 'numbered'` | No | `'progressBar'` | Step indicator style |
563
570
  | `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 |
571
+ | `columnMatching` | `{ type: "managed"; exact?: boolean; caseInsensitive?: boolean; normalized?: boolean; inference?: boolean } \| { type: "custom"; columnMatchHandler: (...) => Promise<...> }` | No | `undefined` | Configure managed column matching or provide a custom matcher |
572
+ | `promptedEdits` | `{ type: "managed" } \| { type: "custom"; promptedEditHandler: (...) => Promise<...> }` | No | `undefined` | Enable managed prompted edits or provide a custom edit handler |
566
573
  | `templateDownload` | `TemplateDownloadOptions<TSchema>` | No | - | Template download configuration with optional schema-typed example rows |
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` |
574
+ | `sessionRecovery` | `SessionRecoveryOptions` | No | - | Enable Recovered sessions with the built-in local backend or a custom adapter implementing `get`, `set`, and `remove` |
568
575
  | `locale` | `DeepPartial<ExpressCSVLocaleInput>` | No | - | Localization overrides |
569
576
  | `disableStatusStep` | `boolean` | No | - | Skip the success/error status screen |
570
577
 
571
578
  #### `open(options)`
572
579
 
573
- Opens the importer and begins the import flow.
580
+ Opens the importer.
574
581
 
575
582
  ```typescript
576
583
  importer.open(options: OpenOptions): void
@@ -578,14 +585,13 @@ importer.open(options: OpenOptions): void
578
585
 
579
586
  | Option | Type | Required | Description |
580
587
  |---|---|---|---|
581
- | `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk of records. Call `next()` to continue. |
582
- | `chunkSize` | `number` | No | Records per chunk (default: 1000) |
588
+ | `onData` | `(chunk: RecordsChunk<T>, next: () => void) => void` | Yes | Callback for each delivered chunk of records. Call `next()` after your backend accepts the current chunk. |
589
+ | `chunkSize` | `ChunkSize` | No | Delivery packet size. Defaults to `{ unit: "kb", value: 500 }`. `kb` uses decimal kilobytes (`1 KB = 1000 bytes`). Use `{ unit: "kb", value: 500 }` for KB or `{ unit: "rows", value: 500 }` for row counts. Zero or negative values send all records in one chunk. |
583
590
  | `onComplete` | `(context: { sessionId: string }) => void` | No | Called when all chunks have been processed |
584
591
  | `onCancel` | `(context: { sessionId: string }) => void` | No | Called when the user cancels the import |
585
592
  | `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 |
588
- | `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the wizard step changes |
593
+ | `onOpen` | `(context: { sessionId: string }) => void` | No | Called when the importer opens |
594
+ | `onStepChange` | `(stepId, previousStepId?) => void` | No | Called when the importer step changes |
589
595
 
590
596
  #### `close(reason?)`
591
597
 
@@ -601,13 +607,13 @@ await importer.close(reason?: 'user_close' | 'cancel' | 'complete' | 'error'): P
601
607
 
602
608
  | Method | Returns | Description |
603
609
  |---|---|---|
604
- | `getState()` | `ImporterState` | Current importer lifecycle state |
610
+ | `getStatus()` | `ImporterStatus` | Current importer lifecycle status |
605
611
  | `getIsReady()` | `boolean` | Whether the importer is ready or open |
606
612
  | `getIsOpen()` | `boolean` | Whether the importer is currently open |
607
613
  | `getConnectionStatus()` | `boolean` | Whether the iframe connection is active |
608
614
  | `getCanRestart()` | `boolean` | Whether the importer can be restarted |
609
615
  | `getLastError()` | `Error \| null` | Last error, if any |
610
- | `getStatus()` | `object` | Comprehensive status snapshot |
616
+ | `getStatusSnapshot()` | `object` | Comprehensive status snapshot |
611
617
 
612
618
  #### `restart(newOptions?)`
613
619
 
@@ -621,10 +627,10 @@ The object passed to `onData` callbacks:
621
627
  interface RecordsChunk<T> {
622
628
  records: T[]; // Automatically typed to your schema
623
629
  totalChunks: number;
624
- currentChunkIndex: number;
630
+ chunkIndex: number;
625
631
  totalRecords: number;
626
632
  sessionId: string;
627
- chunkIdempotencyKey: string;
633
+ deliveryId: string;
628
634
  }
629
635
  ```
630
636
 
@@ -647,7 +653,7 @@ type Row = Infer<typeof schema>;
647
653
  const importer = new CSVImporter({
648
654
  schema,
649
655
  getSessionToken: async () => fetchSessionToken(),
650
- importIdentifier: "user-import",
656
+ importNamespace: "user-import",
651
657
  });
652
658
 
653
659
  importer.open({