@fluentcommerce/fc-connect-sdk 0.1.48 → 0.1.52
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/CHANGELOG.md +506 -379
- package/README.md +343 -0
- package/dist/cjs/clients/fluent-client.js +110 -14
- package/dist/cjs/data-sources/s3-data-source.js +1 -1
- package/dist/cjs/data-sources/sftp-data-source.js +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/services/extraction/extraction-orchestrator.d.ts +4 -1
- package/dist/cjs/services/extraction/extraction-orchestrator.js +84 -11
- package/dist/cjs/types/index.d.ts +79 -10
- package/dist/cjs/versori/fluent-versori-client.d.ts +4 -1
- package/dist/cjs/versori/fluent-versori-client.js +131 -13
- package/dist/esm/clients/fluent-client.js +110 -14
- package/dist/esm/data-sources/s3-data-source.js +1 -1
- package/dist/esm/data-sources/sftp-data-source.js +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/services/extraction/extraction-orchestrator.d.ts +4 -1
- package/dist/esm/services/extraction/extraction-orchestrator.js +84 -11
- package/dist/esm/types/index.d.ts +79 -10
- package/dist/esm/versori/fluent-versori-client.d.ts +4 -1
- package/dist/esm/versori/fluent-versori-client.js +131 -13
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/services/extraction/extraction-orchestrator.d.ts +4 -1
- package/dist/types/types/index.d.ts +79 -10
- package/dist/types/versori/fluent-versori-client.d.ts +4 -1
- package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +478 -18
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +83 -0
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +52 -0
- package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -0
- package/docs/02-CORE-GUIDES/api-reference/readme.md +1 -1
- package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +68 -4
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-01-foundations.md +450 -448
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-02-quick-start.md +476 -474
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-03-schema-validation.md +464 -462
- package/docs/02-CORE-GUIDES/mapping/modules/mapping-05-advanced-patterns.md +1366 -1364
- package/docs/readme.md +245 -245
- package/package.json +17 -6
- package/docs/versori-apis/ACTIVATIONS-AND-VARIABLES-GUIDE.md +0 -60
- package/docs/versori-apis/JWT-GENERATION-GUIDE.md +0 -94
- package/docs/versori-apis/QUICK-WORKFLOW.md +0 -293
- package/docs/versori-apis/README.md +0 -73
- package/docs/versori-apis/VERSORI-PLATFORM-ARCHITECTURE.md +0 -880
- package/docs/versori-apis/Versori-Platform-API.postman_collection.json +0 -2925
- package/docs/versori-apis/Versori-Platform-API.postman_environment.example.json +0 -62
- package/docs/versori-apis/Versori-Platform-API.postman_environment.json +0 -178
|
@@ -1,474 +1,476 @@
|
|
|
1
|
-
# Module 2: Quick Start
|
|
2
|
-
|
|
3
|
-
**Get started with universal mapping in 15 minutes**
|
|
4
|
-
|
|
5
|
-
**Level:** Beginner
|
|
6
|
-
**Time:** 15 minutes
|
|
7
|
-
**Prerequisites:** [Module 1: Foundations](./mapping-01-foundations.md)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Learning Objectives
|
|
12
|
-
|
|
13
|
-
After completing this module, you will:
|
|
14
|
-
|
|
15
|
-
- ✅ Create your first mapping configuration
|
|
16
|
-
- ✅ Use basic SDK resolvers
|
|
17
|
-
- ✅ Run a complete CSV ingestion example
|
|
18
|
-
- ✅ Handle errors and validate results
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
## Your First Mapping
|
|
23
|
-
|
|
24
|
-
Let's create a simple CSV inventory ingestion mapping.
|
|
25
|
-
|
|
26
|
-
### Step 1: Define the Mapping
|
|
27
|
-
|
|
28
|
-
Create `inventory-mapping.json`:
|
|
29
|
-
|
|
30
|
-
```json
|
|
31
|
-
{
|
|
32
|
-
"fields": {
|
|
33
|
-
"ref": {
|
|
34
|
-
"source": "sku",
|
|
35
|
-
"required": true
|
|
36
|
-
},
|
|
37
|
-
"productRef": {
|
|
38
|
-
"source": "sku",
|
|
39
|
-
"required": true
|
|
40
|
-
},
|
|
41
|
-
"locationRef": {
|
|
42
|
-
"source": "location",
|
|
43
|
-
"required": true
|
|
44
|
-
},
|
|
45
|
-
"qty": {
|
|
46
|
-
"source": "quantity",
|
|
47
|
-
"resolver": "sdk.parseInt",
|
|
48
|
-
"required": true
|
|
49
|
-
},
|
|
50
|
-
"status": {
|
|
51
|
-
"source": "status",
|
|
52
|
-
"resolver": "sdk.uppercase",
|
|
53
|
-
"defaultValue": "AVAILABLE"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Step 2: Prepare Sample Data
|
|
60
|
-
|
|
61
|
-
Create `inventory.csv`:
|
|
62
|
-
|
|
63
|
-
```csv
|
|
64
|
-
sku,location,quantity,status
|
|
65
|
-
SKU-WM-001,WH-001,500,available
|
|
66
|
-
SKU-WM-002,WH-001,150,available
|
|
67
|
-
SKU-KB-001,WH-001,75,reserved
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Step 3: Implement the Workflow
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { UniversalMapper, CSVParserService, createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
74
|
-
import * as fs from 'fs';
|
|
75
|
-
|
|
76
|
-
// Load mapping configuration
|
|
77
|
-
const mappingConfig = JSON.parse(fs.readFileSync('./inventory-mapping.json', 'utf-8'));
|
|
78
|
-
|
|
79
|
-
// Load CSV data
|
|
80
|
-
const csvContent = fs.readFileSync('./inventory.csv', 'utf-8');
|
|
81
|
-
const parser = new CSVParserService();
|
|
82
|
-
const records = await parser.parse(csvContent);
|
|
83
|
-
|
|
84
|
-
// Create mapper
|
|
85
|
-
const mapper = new UniversalMapper(mappingConfig);
|
|
86
|
-
|
|
87
|
-
// Process each CSV row
|
|
88
|
-
const batchEntities = [];
|
|
89
|
-
for (const row of records) {
|
|
90
|
-
const result = await mapper.map(row);
|
|
91
|
-
|
|
92
|
-
if (result.success) {
|
|
93
|
-
batchEntities.push(result.data);
|
|
94
|
-
} else {
|
|
95
|
-
console.error('Mapping failed for row:', row);
|
|
96
|
-
console.error('Errors:', result.errors);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Send to Fluent Commerce
|
|
101
|
-
const client = await createClient({
|
|
102
|
-
config: {
|
|
103
|
-
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
104
|
-
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
105
|
-
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
106
|
-
username: process.env.FLUENT_USERNAME!,
|
|
107
|
-
password: process.env.FLUENT_PASSWORD!,
|
|
108
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Create a batch job and send entities
|
|
113
|
-
const job = await client.createJob({
|
|
114
|
-
name: `inventory-import-${Date.now()}`,
|
|
115
|
-
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
await client.sendBatch(job.id, {
|
|
119
|
-
action: 'UPSERT',
|
|
120
|
-
entityType: 'INVENTORY',
|
|
121
|
-
source: 'CSV_IMPORT',
|
|
122
|
-
event: 'INVENTORY_UPDATE',
|
|
123
|
-
entities: batchEntities,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
console.log(`✅ Successfully processed ${batchEntities.length} records`);
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Step 4: Run and Validate
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
npm install @fluentcommerce/fc-connect-sdk
|
|
133
|
-
node inventory-import.js
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Expected Output:**
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
✅ Successfully processed 3 records
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Understanding the Mapping
|
|
145
|
-
|
|
146
|
-
Let's break down what each field does:
|
|
147
|
-
|
|
148
|
-
### Field: `ref`
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
"ref": {
|
|
152
|
-
"source": "sku",
|
|
153
|
-
"required": true
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
- **source:** Read from `sku` column in CSV
|
|
158
|
-
- **required:** Must have a value (fails if missing)
|
|
159
|
-
- **Result:** Direct copy of SKU value
|
|
160
|
-
|
|
161
|
-
### Field: `qty`
|
|
162
|
-
|
|
163
|
-
```json
|
|
164
|
-
"qty": {
|
|
165
|
-
"source": "quantity",
|
|
166
|
-
"resolver": "sdk.parseInt",
|
|
167
|
-
"required": true
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
- **source:** Read from `quantity` column
|
|
172
|
-
- **resolver:** Convert string "500" to integer 500
|
|
173
|
-
- **required:** Must have a value
|
|
174
|
-
- **Result:** Parsed integer
|
|
175
|
-
|
|
176
|
-
### Field: `status`
|
|
177
|
-
|
|
178
|
-
```json
|
|
179
|
-
"status": {
|
|
180
|
-
"source": "status",
|
|
181
|
-
"resolver": "sdk.uppercase",
|
|
182
|
-
"defaultValue": "AVAILABLE"
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
- **source:** Read from `status` column
|
|
187
|
-
- **resolver:** Convert "available" to "AVAILABLE"
|
|
188
|
-
- **defaultValue:** Use "AVAILABLE" if missing
|
|
189
|
-
- **Result:** Uppercased string or default
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Common SDK Resolvers
|
|
194
|
-
|
|
195
|
-
Here are the most commonly used SDK resolvers:
|
|
196
|
-
|
|
197
|
-
### String Transformations
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
// Uppercase
|
|
201
|
-
"status": { "source": "status", "resolver": "sdk.uppercase" }
|
|
202
|
-
// "active" → "ACTIVE"
|
|
203
|
-
|
|
204
|
-
// Lowercase
|
|
205
|
-
"email": { "source": "email", "resolver": "sdk.lowercase" }
|
|
206
|
-
// "USER@EXAMPLE.COM" → "user@example.com"
|
|
207
|
-
|
|
208
|
-
// Trim whitespace
|
|
209
|
-
"name": { "source": "name", "resolver": "sdk.trim" }
|
|
210
|
-
// " Product " → "Product"
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Number Transformations
|
|
214
|
-
|
|
215
|
-
```json
|
|
216
|
-
// Parse integer
|
|
217
|
-
"quantity": { "source": "qty", "resolver": "sdk.parseInt" }
|
|
218
|
-
// "100" → 100
|
|
219
|
-
|
|
220
|
-
// Parse float
|
|
221
|
-
"price": { "source": "price", "resolver": "sdk.parseFloat" }
|
|
222
|
-
// "19.99" → 19.99
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Date Transformations
|
|
226
|
-
|
|
227
|
-
```json
|
|
228
|
-
// ISO8601 format
|
|
229
|
-
"createdAt": { "source": "date", "resolver": "sdk.formatDate" }
|
|
230
|
-
// "2024-01-15" → "2024-01-15T00:00:00.000Z"
|
|
231
|
-
|
|
232
|
-
// Short format (YYYY-MM-DD)
|
|
233
|
-
"expectedOn": { "source": "date", "resolver": "sdk.formatDateShort" }
|
|
234
|
-
// "01/15/2024" → "2024-01-15"
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## Error Handling
|
|
240
|
-
|
|
241
|
-
Always check mapping results:
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
const result = await mapper.map(sourceData);
|
|
245
|
-
|
|
246
|
-
if (result.success) {
|
|
247
|
-
// ✅ Mapping succeeded
|
|
248
|
-
console.log('Mapped data:', result.data);
|
|
249
|
-
batchEntities.push(result.data);
|
|
250
|
-
} else {
|
|
251
|
-
// ❌ Mapping failed
|
|
252
|
-
console.error('Mapping failed');
|
|
253
|
-
console.error('Errors:', result.errors);
|
|
254
|
-
console.error('Source data:', sourceData);
|
|
255
|
-
|
|
256
|
-
// Log each error
|
|
257
|
-
result.errors.forEach(error => {
|
|
258
|
-
console.error(`- Field: ${error.field}`);
|
|
259
|
-
console.error(` Message: ${error.message}`);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Common Errors
|
|
265
|
-
|
|
266
|
-
**Missing Required Field:**
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
Error: Required field 'productRef' is missing
|
|
270
|
-
Source: { sku: '', location: 'WH-001', quantity: '100' }
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Invalid Number:**
|
|
274
|
-
|
|
275
|
-
```
|
|
276
|
-
Error: Failed to parse 'qty' as integer
|
|
277
|
-
Source: { quantity: 'invalid' }
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
**Resolver Error:**
|
|
281
|
-
|
|
282
|
-
```
|
|
283
|
-
Error: Resolver 'sdk.parseInt' failed
|
|
284
|
-
Message: Cannot parse 'abc' as integer
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## Static Values
|
|
290
|
-
|
|
291
|
-
Sometimes you need constant values:
|
|
292
|
-
|
|
293
|
-
```json
|
|
294
|
-
{
|
|
295
|
-
"fields": {
|
|
296
|
-
"type": {
|
|
297
|
-
"value": "STANDARD"
|
|
298
|
-
},
|
|
299
|
-
"retailerId": {
|
|
300
|
-
"value": 1
|
|
301
|
-
},
|
|
302
|
-
"source": {
|
|
303
|
-
"value": "CSV_IMPORT"
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
**Result:**
|
|
310
|
-
|
|
311
|
-
```json
|
|
312
|
-
{
|
|
313
|
-
"type": "STANDARD",
|
|
314
|
-
"retailerId": 1,
|
|
315
|
-
"source": "CSV_IMPORT"
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Default Values
|
|
322
|
-
|
|
323
|
-
Provide fallbacks for optional fields:
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
{
|
|
327
|
-
"fields": {
|
|
328
|
-
"status": {
|
|
329
|
-
"source": "orderStatus",
|
|
330
|
-
"defaultValue": "PENDING"
|
|
331
|
-
},
|
|
332
|
-
"shippingMethod": {
|
|
333
|
-
"source": "shipping",
|
|
334
|
-
"defaultValue": "STANDARD"
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Behavior:**
|
|
341
|
-
|
|
342
|
-
- If `orderStatus` exists → use its value
|
|
343
|
-
- If `orderStatus` is missing/empty → use "PENDING"
|
|
344
|
-
|
|
345
|
-
---
|
|
346
|
-
|
|
347
|
-
## Combining Resolvers and Defaults
|
|
348
|
-
|
|
349
|
-
```json
|
|
350
|
-
{
|
|
351
|
-
"fields": {
|
|
352
|
-
"quantity": {
|
|
353
|
-
"source": "qty",
|
|
354
|
-
"resolver": "sdk.parseInt",
|
|
355
|
-
"defaultValue": 0
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Processing order:**
|
|
362
|
-
|
|
363
|
-
1. Read from `qty` field
|
|
364
|
-
2. If missing → use default value `0`
|
|
365
|
-
3. Apply resolver `sdk.parseInt`
|
|
366
|
-
4. Return result
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
## Practice Exercise
|
|
371
|
-
|
|
372
|
-
**Task:** Create a mapping for this customer CSV:
|
|
373
|
-
|
|
374
|
-
```csv
|
|
375
|
-
email,first_name,last_name,phone,status
|
|
376
|
-
john@example.com,John,Doe,555-1234,active
|
|
377
|
-
jane@example.com,Jane,Smith,555-5678,
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
**Requirements:**
|
|
381
|
-
|
|
382
|
-
1. Map `email` to lowercase
|
|
383
|
-
2. Combine `first_name` and `last_name` into `fullName`
|
|
384
|
-
3. Format `phone` (you'll need a custom resolver)
|
|
385
|
-
4. Map `status` to uppercase with default "ACTIVE"
|
|
386
|
-
5. Mark `email` as required
|
|
387
|
-
|
|
388
|
-
<details>
|
|
389
|
-
<summary>Click to see solution</summary>
|
|
390
|
-
|
|
391
|
-
```json
|
|
392
|
-
{
|
|
393
|
-
"fields": {
|
|
394
|
-
"email": {
|
|
395
|
-
"source": "email",
|
|
396
|
-
"resolver": "sdk.lowercase",
|
|
397
|
-
"required": true
|
|
398
|
-
},
|
|
399
|
-
"firstName": {
|
|
400
|
-
"source": "first_name",
|
|
401
|
-
"required": true
|
|
402
|
-
},
|
|
403
|
-
"lastName": {
|
|
404
|
-
"source": "last_name",
|
|
405
|
-
"required": true
|
|
406
|
-
},
|
|
407
|
-
"phone": {
|
|
408
|
-
"source": "phone",
|
|
409
|
-
"resolver": "custom.formatPhone"
|
|
410
|
-
},
|
|
411
|
-
"status": {
|
|
412
|
-
"source": "status",
|
|
413
|
-
"resolver": "sdk.uppercase",
|
|
414
|
-
"defaultValue": "ACTIVE"
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
**Custom resolver for phone formatting:**
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
const customResolvers = {
|
|
424
|
-
'custom.formatPhone': (value: any) => {
|
|
425
|
-
const digits = String(value).replace(/\D/g, '');
|
|
426
|
-
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const mapper = new UniversalMapper(config, { customResolvers });
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
**Note:** Combining `first_name` and `last_name` would also require a custom resolver. See [Module 6: Custom Resolvers](./mapping-06-helpers-resolvers.md) for advanced patterns.
|
|
434
|
-
|
|
435
|
-
</details>
|
|
436
|
-
|
|
437
|
-
---
|
|
438
|
-
|
|
439
|
-
## Key Takeaways
|
|
440
|
-
|
|
441
|
-
1. **Start simple:** Basic mappings only need `source` and optional `resolver`
|
|
442
|
-
2. **Mark required fields:** Use `required: true` to catch missing data early
|
|
443
|
-
3. **Use SDK resolvers:** Built-in resolvers cover 90% of common transformations
|
|
444
|
-
4. **Always check results:** Validate `result.success` before using data
|
|
445
|
-
5. **Provide defaults:** Use `defaultValue` for optional fields
|
|
446
|
-
|
|
447
|
-
---
|
|
448
|
-
|
|
449
|
-
## Next Steps
|
|
450
|
-
|
|
451
|
-
Now that you can create basic mappings, you're ready to ensure they're correct:
|
|
452
|
-
|
|
453
|
-
→ Continue to [Module 3: Schema Validation](./mapping-03-schema-validation.md)
|
|
454
|
-
|
|
455
|
-
**Alternative paths:**
|
|
456
|
-
|
|
457
|
-
- Jump to [Use Cases](./mapping-04-use-cases.md) to see real-world examples
|
|
458
|
-
- Review [Advanced Patterns](./mapping-05-advanced-patterns.md) for nested objects and arrays
|
|
459
|
-
|
|
460
|
-
---
|
|
461
|
-
|
|
462
|
-
[← Back to Foundations](./mapping-01-foundations.md) | [Next: Schema Validation →](./mapping-03-schema-validation.md)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1
|
+
# Module 2: Quick Start
|
|
2
|
+
|
|
3
|
+
**Get started with universal mapping in 15 minutes**
|
|
4
|
+
|
|
5
|
+
**Level:** Beginner
|
|
6
|
+
**Time:** 15 minutes
|
|
7
|
+
**Prerequisites:** [Module 1: Foundations](./mapping-01-foundations.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Learning Objectives
|
|
12
|
+
|
|
13
|
+
After completing this module, you will:
|
|
14
|
+
|
|
15
|
+
- ✅ Create your first mapping configuration
|
|
16
|
+
- ✅ Use basic SDK resolvers
|
|
17
|
+
- ✅ Run a complete CSV ingestion example
|
|
18
|
+
- ✅ Handle errors and validate results
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Your First Mapping
|
|
23
|
+
|
|
24
|
+
Let's create a simple CSV inventory ingestion mapping.
|
|
25
|
+
|
|
26
|
+
### Step 1: Define the Mapping
|
|
27
|
+
|
|
28
|
+
Create `inventory-mapping.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"fields": {
|
|
33
|
+
"ref": {
|
|
34
|
+
"source": "sku",
|
|
35
|
+
"required": true
|
|
36
|
+
},
|
|
37
|
+
"productRef": {
|
|
38
|
+
"source": "sku",
|
|
39
|
+
"required": true
|
|
40
|
+
},
|
|
41
|
+
"locationRef": {
|
|
42
|
+
"source": "location",
|
|
43
|
+
"required": true
|
|
44
|
+
},
|
|
45
|
+
"qty": {
|
|
46
|
+
"source": "quantity",
|
|
47
|
+
"resolver": "sdk.parseInt",
|
|
48
|
+
"required": true
|
|
49
|
+
},
|
|
50
|
+
"status": {
|
|
51
|
+
"source": "status",
|
|
52
|
+
"resolver": "sdk.uppercase",
|
|
53
|
+
"defaultValue": "AVAILABLE"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Step 2: Prepare Sample Data
|
|
60
|
+
|
|
61
|
+
Create `inventory.csv`:
|
|
62
|
+
|
|
63
|
+
```csv
|
|
64
|
+
sku,location,quantity,status
|
|
65
|
+
SKU-WM-001,WH-001,500,available
|
|
66
|
+
SKU-WM-002,WH-001,150,available
|
|
67
|
+
SKU-KB-001,WH-001,75,reserved
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 3: Implement the Workflow
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { UniversalMapper, CSVParserService, createClient } from '@fluentcommerce/fc-connect-sdk';
|
|
74
|
+
import * as fs from 'fs';
|
|
75
|
+
|
|
76
|
+
// Load mapping configuration
|
|
77
|
+
const mappingConfig = JSON.parse(fs.readFileSync('./inventory-mapping.json', 'utf-8'));
|
|
78
|
+
|
|
79
|
+
// Load CSV data
|
|
80
|
+
const csvContent = fs.readFileSync('./inventory.csv', 'utf-8');
|
|
81
|
+
const parser = new CSVParserService();
|
|
82
|
+
const records = await parser.parse(csvContent);
|
|
83
|
+
|
|
84
|
+
// Create mapper
|
|
85
|
+
const mapper = new UniversalMapper(mappingConfig);
|
|
86
|
+
|
|
87
|
+
// Process each CSV row
|
|
88
|
+
const batchEntities = [];
|
|
89
|
+
for (const row of records) {
|
|
90
|
+
const result = await mapper.map(row);
|
|
91
|
+
|
|
92
|
+
if (result.success) {
|
|
93
|
+
batchEntities.push(result.data);
|
|
94
|
+
} else {
|
|
95
|
+
console.error('Mapping failed for row:', row);
|
|
96
|
+
console.error('Errors:', result.errors);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Send to Fluent Commerce
|
|
101
|
+
const client = await createClient({
|
|
102
|
+
config: {
|
|
103
|
+
baseUrl: process.env.FLUENT_BASE_URL!,
|
|
104
|
+
clientId: process.env.FLUENT_CLIENT_ID!,
|
|
105
|
+
clientSecret: process.env.FLUENT_CLIENT_SECRET!,
|
|
106
|
+
username: process.env.FLUENT_USERNAME!,
|
|
107
|
+
password: process.env.FLUENT_PASSWORD!,
|
|
108
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Create a batch job and send entities
|
|
113
|
+
const job = await client.createJob({
|
|
114
|
+
name: `inventory-import-${Date.now()}`,
|
|
115
|
+
retailerId: process.env.FLUENT_RETAILER_ID!,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await client.sendBatch(job.id, {
|
|
119
|
+
action: 'UPSERT',
|
|
120
|
+
entityType: 'INVENTORY',
|
|
121
|
+
source: 'CSV_IMPORT',
|
|
122
|
+
event: 'INVENTORY_UPDATE',
|
|
123
|
+
entities: batchEntities,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log(`✅ Successfully processed ${batchEntities.length} records`);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Step 4: Run and Validate
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm install @fluentcommerce/fc-connect-sdk
|
|
133
|
+
node inventory-import.js
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Expected Output:**
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
✅ Successfully processed 3 records
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Understanding the Mapping
|
|
145
|
+
|
|
146
|
+
Let's break down what each field does:
|
|
147
|
+
|
|
148
|
+
### Field: `ref`
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
"ref": {
|
|
152
|
+
"source": "sku",
|
|
153
|
+
"required": true
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **source:** Read from `sku` column in CSV
|
|
158
|
+
- **required:** Must have a value (fails if missing)
|
|
159
|
+
- **Result:** Direct copy of SKU value
|
|
160
|
+
|
|
161
|
+
### Field: `qty`
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
"qty": {
|
|
165
|
+
"source": "quantity",
|
|
166
|
+
"resolver": "sdk.parseInt",
|
|
167
|
+
"required": true
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **source:** Read from `quantity` column
|
|
172
|
+
- **resolver:** Convert string "500" to integer 500
|
|
173
|
+
- **required:** Must have a value
|
|
174
|
+
- **Result:** Parsed integer
|
|
175
|
+
|
|
176
|
+
### Field: `status`
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"status": {
|
|
180
|
+
"source": "status",
|
|
181
|
+
"resolver": "sdk.uppercase",
|
|
182
|
+
"defaultValue": "AVAILABLE"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- **source:** Read from `status` column
|
|
187
|
+
- **resolver:** Convert "available" to "AVAILABLE"
|
|
188
|
+
- **defaultValue:** Use "AVAILABLE" if missing
|
|
189
|
+
- **Result:** Uppercased string or default
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Common SDK Resolvers
|
|
194
|
+
|
|
195
|
+
Here are the most commonly used SDK resolvers:
|
|
196
|
+
|
|
197
|
+
### String Transformations
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
// Uppercase
|
|
201
|
+
"status": { "source": "status", "resolver": "sdk.uppercase" }
|
|
202
|
+
// "active" → "ACTIVE"
|
|
203
|
+
|
|
204
|
+
// Lowercase
|
|
205
|
+
"email": { "source": "email", "resolver": "sdk.lowercase" }
|
|
206
|
+
// "USER@EXAMPLE.COM" → "user@example.com"
|
|
207
|
+
|
|
208
|
+
// Trim whitespace
|
|
209
|
+
"name": { "source": "name", "resolver": "sdk.trim" }
|
|
210
|
+
// " Product " → "Product"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Number Transformations
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
// Parse integer
|
|
217
|
+
"quantity": { "source": "qty", "resolver": "sdk.parseInt" }
|
|
218
|
+
// "100" → 100
|
|
219
|
+
|
|
220
|
+
// Parse float
|
|
221
|
+
"price": { "source": "price", "resolver": "sdk.parseFloat" }
|
|
222
|
+
// "19.99" → 19.99
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Date Transformations
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
// ISO8601 format
|
|
229
|
+
"createdAt": { "source": "date", "resolver": "sdk.formatDate" }
|
|
230
|
+
// "2024-01-15" → "2024-01-15T00:00:00.000Z"
|
|
231
|
+
|
|
232
|
+
// Short format (YYYY-MM-DD)
|
|
233
|
+
"expectedOn": { "source": "date", "resolver": "sdk.formatDateShort" }
|
|
234
|
+
// "01/15/2024" → "2024-01-15"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Error Handling
|
|
240
|
+
|
|
241
|
+
Always check mapping results:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const result = await mapper.map(sourceData);
|
|
245
|
+
|
|
246
|
+
if (result.success) {
|
|
247
|
+
// ✅ Mapping succeeded
|
|
248
|
+
console.log('Mapped data:', result.data);
|
|
249
|
+
batchEntities.push(result.data);
|
|
250
|
+
} else {
|
|
251
|
+
// ❌ Mapping failed
|
|
252
|
+
console.error('Mapping failed');
|
|
253
|
+
console.error('Errors:', result.errors);
|
|
254
|
+
console.error('Source data:', sourceData);
|
|
255
|
+
|
|
256
|
+
// Log each error
|
|
257
|
+
result.errors.forEach(error => {
|
|
258
|
+
console.error(`- Field: ${error.field}`);
|
|
259
|
+
console.error(` Message: ${error.message}`);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Common Errors
|
|
265
|
+
|
|
266
|
+
**Missing Required Field:**
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Error: Required field 'productRef' is missing
|
|
270
|
+
Source: { sku: '', location: 'WH-001', quantity: '100' }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Invalid Number:**
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
Error: Failed to parse 'qty' as integer
|
|
277
|
+
Source: { quantity: 'invalid' }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Resolver Error:**
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
Error: Resolver 'sdk.parseInt' failed
|
|
284
|
+
Message: Cannot parse 'abc' as integer
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Static Values
|
|
290
|
+
|
|
291
|
+
Sometimes you need constant values:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"fields": {
|
|
296
|
+
"type": {
|
|
297
|
+
"value": "STANDARD"
|
|
298
|
+
},
|
|
299
|
+
"retailerId": {
|
|
300
|
+
"value": 1
|
|
301
|
+
},
|
|
302
|
+
"source": {
|
|
303
|
+
"value": "CSV_IMPORT"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Result:**
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"type": "STANDARD",
|
|
314
|
+
"retailerId": 1,
|
|
315
|
+
"source": "CSV_IMPORT"
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Default Values
|
|
322
|
+
|
|
323
|
+
Provide fallbacks for optional fields:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"fields": {
|
|
328
|
+
"status": {
|
|
329
|
+
"source": "orderStatus",
|
|
330
|
+
"defaultValue": "PENDING"
|
|
331
|
+
},
|
|
332
|
+
"shippingMethod": {
|
|
333
|
+
"source": "shipping",
|
|
334
|
+
"defaultValue": "STANDARD"
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Behavior:**
|
|
341
|
+
|
|
342
|
+
- If `orderStatus` exists → use its value
|
|
343
|
+
- If `orderStatus` is missing/empty → use "PENDING"
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Combining Resolvers and Defaults
|
|
348
|
+
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"fields": {
|
|
352
|
+
"quantity": {
|
|
353
|
+
"source": "qty",
|
|
354
|
+
"resolver": "sdk.parseInt",
|
|
355
|
+
"defaultValue": 0
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Processing order:**
|
|
362
|
+
|
|
363
|
+
1. Read from `qty` field
|
|
364
|
+
2. If missing → use default value `0`
|
|
365
|
+
3. Apply resolver `sdk.parseInt`
|
|
366
|
+
4. Return result
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Practice Exercise
|
|
371
|
+
|
|
372
|
+
**Task:** Create a mapping for this customer CSV:
|
|
373
|
+
|
|
374
|
+
```csv
|
|
375
|
+
email,first_name,last_name,phone,status
|
|
376
|
+
john@example.com,John,Doe,555-1234,active
|
|
377
|
+
jane@example.com,Jane,Smith,555-5678,
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**Requirements:**
|
|
381
|
+
|
|
382
|
+
1. Map `email` to lowercase
|
|
383
|
+
2. Combine `first_name` and `last_name` into `fullName`
|
|
384
|
+
3. Format `phone` (you'll need a custom resolver)
|
|
385
|
+
4. Map `status` to uppercase with default "ACTIVE"
|
|
386
|
+
5. Mark `email` as required
|
|
387
|
+
|
|
388
|
+
<details>
|
|
389
|
+
<summary>Click to see solution</summary>
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
{
|
|
393
|
+
"fields": {
|
|
394
|
+
"email": {
|
|
395
|
+
"source": "email",
|
|
396
|
+
"resolver": "sdk.lowercase",
|
|
397
|
+
"required": true
|
|
398
|
+
},
|
|
399
|
+
"firstName": {
|
|
400
|
+
"source": "first_name",
|
|
401
|
+
"required": true
|
|
402
|
+
},
|
|
403
|
+
"lastName": {
|
|
404
|
+
"source": "last_name",
|
|
405
|
+
"required": true
|
|
406
|
+
},
|
|
407
|
+
"phone": {
|
|
408
|
+
"source": "phone",
|
|
409
|
+
"resolver": "custom.formatPhone"
|
|
410
|
+
},
|
|
411
|
+
"status": {
|
|
412
|
+
"source": "status",
|
|
413
|
+
"resolver": "sdk.uppercase",
|
|
414
|
+
"defaultValue": "ACTIVE"
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Custom resolver for phone formatting:**
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const customResolvers = {
|
|
424
|
+
'custom.formatPhone': (value: any) => {
|
|
425
|
+
const digits = String(value).replace(/\D/g, '');
|
|
426
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const mapper = new UniversalMapper(config, { customResolvers });
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Note:** Combining `first_name` and `last_name` would also require a custom resolver. See [Module 6: Custom Resolvers](./mapping-06-helpers-resolvers.md) for advanced patterns.
|
|
434
|
+
|
|
435
|
+
</details>
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Key Takeaways
|
|
440
|
+
|
|
441
|
+
1. **Start simple:** Basic mappings only need `source` and optional `resolver`
|
|
442
|
+
2. **Mark required fields:** Use `required: true` to catch missing data early
|
|
443
|
+
3. **Use SDK resolvers:** Built-in resolvers cover 90% of common transformations
|
|
444
|
+
4. **Always check results:** Validate `result.success` before using data
|
|
445
|
+
5. **Provide defaults:** Use `defaultValue` for optional fields
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Next Steps
|
|
450
|
+
|
|
451
|
+
Now that you can create basic mappings, you're ready to ensure they're correct:
|
|
452
|
+
|
|
453
|
+
→ Continue to [Module 3: Schema Validation](./mapping-03-schema-validation.md)
|
|
454
|
+
|
|
455
|
+
**Alternative paths:**
|
|
456
|
+
|
|
457
|
+
- Jump to [Use Cases](./mapping-04-use-cases.md) to see real-world examples
|
|
458
|
+
- Review [Advanced Patterns](./mapping-05-advanced-patterns.md) for nested objects and arrays
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
[← Back to Foundations](./mapping-01-foundations.md) | [Next: Schema Validation →](./mapping-03-schema-validation.md)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
|