@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +506 -379
  2. package/README.md +343 -0
  3. package/dist/cjs/clients/fluent-client.js +110 -14
  4. package/dist/cjs/data-sources/s3-data-source.js +1 -1
  5. package/dist/cjs/data-sources/sftp-data-source.js +1 -1
  6. package/dist/cjs/index.d.ts +1 -1
  7. package/dist/cjs/services/extraction/extraction-orchestrator.d.ts +4 -1
  8. package/dist/cjs/services/extraction/extraction-orchestrator.js +84 -11
  9. package/dist/cjs/types/index.d.ts +79 -10
  10. package/dist/cjs/versori/fluent-versori-client.d.ts +4 -1
  11. package/dist/cjs/versori/fluent-versori-client.js +131 -13
  12. package/dist/esm/clients/fluent-client.js +110 -14
  13. package/dist/esm/data-sources/s3-data-source.js +1 -1
  14. package/dist/esm/data-sources/sftp-data-source.js +1 -1
  15. package/dist/esm/index.d.ts +1 -1
  16. package/dist/esm/services/extraction/extraction-orchestrator.d.ts +4 -1
  17. package/dist/esm/services/extraction/extraction-orchestrator.js +84 -11
  18. package/dist/esm/types/index.d.ts +79 -10
  19. package/dist/esm/versori/fluent-versori-client.d.ts +4 -1
  20. package/dist/esm/versori/fluent-versori-client.js +131 -13
  21. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/tsconfig.types.tsbuildinfo +1 -1
  24. package/dist/types/index.d.ts +1 -1
  25. package/dist/types/services/extraction/extraction-orchestrator.d.ts +4 -1
  26. package/dist/types/types/index.d.ts +79 -10
  27. package/dist/types/versori/fluent-versori-client.d.ts +4 -1
  28. package/docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md +478 -18
  29. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-01-client-api.md +83 -0
  30. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-08-types.md +52 -0
  31. package/docs/02-CORE-GUIDES/api-reference/modules/api-reference-12-partial-responses.md +212 -0
  32. package/docs/02-CORE-GUIDES/api-reference/readme.md +1 -1
  33. package/docs/02-CORE-GUIDES/extraction/modules/02-core-guides-extraction-08-extraction-orchestrator.md +68 -4
  34. package/docs/02-CORE-GUIDES/mapping/modules/mapping-01-foundations.md +450 -448
  35. package/docs/02-CORE-GUIDES/mapping/modules/mapping-02-quick-start.md +476 -474
  36. package/docs/02-CORE-GUIDES/mapping/modules/mapping-03-schema-validation.md +464 -462
  37. package/docs/02-CORE-GUIDES/mapping/modules/mapping-05-advanced-patterns.md +1366 -1364
  38. package/docs/readme.md +245 -245
  39. package/package.json +17 -6
  40. package/docs/versori-apis/ACTIVATIONS-AND-VARIABLES-GUIDE.md +0 -60
  41. package/docs/versori-apis/JWT-GENERATION-GUIDE.md +0 -94
  42. package/docs/versori-apis/QUICK-WORKFLOW.md +0 -293
  43. package/docs/versori-apis/README.md +0 -73
  44. package/docs/versori-apis/VERSORI-PLATFORM-ARCHITECTURE.md +0 -880
  45. package/docs/versori-apis/Versori-Platform-API.postman_collection.json +0 -2925
  46. package/docs/versori-apis/Versori-Platform-API.postman_environment.example.json +0 -62
  47. package/docs/versori-apis/Versori-Platform-API.postman_environment.json +0 -178
package/README.md CHANGED
@@ -32,6 +32,7 @@ TypeScript SDK for building **Fluent Commerce** integrations across Node.js, Den
32
32
  - [Core Services](#core-services) - API reference
33
33
  - [CLI Tools](#cli-tooling) - Command-line utilities
34
34
  - [Authentication & Webhooks](#authentication-webhooks) - Security
35
+ - [Security & Compliance](#security--compliance) - Security audits, SOC 2, vulnerability management 🆕
35
36
  - [Common Pitfalls](#common-pitfalls-and-fixes) - Troubleshooting
36
37
  - [Support](#additional-resources) - Help & resources
37
38
 
@@ -146,6 +147,7 @@ Choose the right Fluent Commerce API for your use case:
146
147
  | **Location setup** (stores, warehouses) | **Event API** | `sendEvent()` | Requires workflow orchestration |
147
148
  | **Order creation** (one-time orders) | **GraphQL** | `client.graphql()` with `createOrder` mutation | Triggers Order CREATED event, full control |
148
149
  | **Order updates/events** (status changes) | **Event API** | `sendEvent()` | Triggers workflows for order state changes |
150
+ | **Audit/search event history** (trace flows, investigate failures) | **Event API (GET)** | `getEvents()` + `getEventById()` | Read-only event/audit retrieval with filters and pagination |
149
151
  | **Customer data** (registration, profiles) | **GraphQL** | `client.graphql()` with mutations | No Rubix workflow support for customers |
150
152
  | **Data extraction** (export to S3/SFTP) | **GraphQL** | `client.graphql()` + `ExtractionOrchestrator` | Query with auto-pagination, extract thousands of records |
151
153
  | **Single operations** (create one product, update one location) | **GraphQL** | `client.graphql()` | Direct control, immediate feedback |
@@ -1114,6 +1116,27 @@ console.log(
1114
1116
  // result.stats = { totalPages: 9, totalRecords: 847, truncated: false }
1115
1117
  ```
1116
1118
 
1119
+ **Handling Partial Responses (Errors with Data):**
1120
+
1121
+ ```typescript
1122
+ // When GraphQL returns errors but some data is available
1123
+ const result = await orchestrator.extract({
1124
+ query: ORDERS_QUERY,
1125
+ resultPath: 'orders.edges.node',
1126
+ errorHandling: 'partial', // Continue extraction even with errors
1127
+ });
1128
+
1129
+ // Check for partial errors
1130
+ if (result.stats.partialErrors) {
1131
+ console.warn(`Extraction completed with ${result.stats.partialErrors.length} errors`);
1132
+ console.log(`Still extracted ${result.data.length} records`);
1133
+ // Errors are in result.stats.partialErrors
1134
+ }
1135
+
1136
+ // Edge case: If GraphQL returns errors with null data, result.data will be []
1137
+ // but errors are still available in result.stats.partialErrors
1138
+ ```
1139
+
1117
1140
  **Alternative: Using FluentClient (For Custom Response Handling)**
1118
1141
 
1119
1142
  ```typescript
@@ -1195,6 +1218,222 @@ console.log('✅ Event sent successfully');
1195
1218
  - **Event API Templates:** `docs/01-TEMPLATES/versori/workflows/ingestion/event-api/` (8 templates for S3/SFTP with CSV, XML, JSON, Parquet)
1196
1219
  - **Batch API Guide:** `docs/02-CORE-GUIDES/ingestion/modules/02-CORE-GUIDES-ingestion-06-batch-api.md`
1197
1220
 
1221
+ ### Event Log API (Search + Audit)
1222
+
1223
+ Query and retrieve events/audit logs from the Fluent Commerce REST Event API. Use these **read-only** methods for troubleshooting workflows, tracing event chains, monitoring orchestration, and auditing batch processing.
1224
+
1225
+ **Methods:**
1226
+ - `getEvents(params)` → `GET /api/v4.1/event` - Search/filter events with flexible query parameters
1227
+ - `getEventById(eventId)` → `GET /api/v4.1/event/{eventId}` - Get a single event by ID
1228
+
1229
+ **Important:** These methods query the Event Log (audit trail). They do NOT trigger workflows — use `sendEvent()` for that.
1230
+
1231
+ #### Basic Usage
1232
+
1233
+ ```typescript
1234
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
1235
+
1236
+ const client = await createClient({
1237
+ config: { baseUrl, clientId, clientSecret, username, password },
1238
+ });
1239
+
1240
+ // 1) Search for failed orchestration audit events on orders
1241
+ const logs = await client.getEvents({
1242
+ 'context.rootEntityType': 'ORDER',
1243
+ eventType: 'ORCHESTRATION_AUDIT',
1244
+ eventStatus: 'FAILED',
1245
+ count: 100,
1246
+ });
1247
+
1248
+ console.log(`Found ${logs.results.length} events (hasMore=${logs.hasMore})`);
1249
+
1250
+ for (const event of logs.results) {
1251
+ console.log(` ${event.name} [${event.eventStatus}] on ${event.context?.entityType}:${event.context?.entityRef}`);
1252
+ }
1253
+
1254
+ // 2) Get full details for a specific event by ID
1255
+ if (logs.results[0]?.id) {
1256
+ const event = await client.getEventById(logs.results[0].id);
1257
+ console.log(`Event: ${event.name} - Status: ${event.eventStatus}`);
1258
+ console.log(` Entity: ${event.context?.entityType}:${event.context?.entityRef}`);
1259
+ console.log(` Root: ${event.context?.rootEntityType}:${event.context?.rootEntityRef}`);
1260
+ console.log(` Generated: ${event.generatedOn} by ${event.generatedBy}`);
1261
+
1262
+ // Trace parent events (for debugging event chains)
1263
+ if (event.context?.sourceEvents?.length) {
1264
+ for (const parentId of event.context.sourceEvents) {
1265
+ const parent = await client.getEventById(parentId);
1266
+ console.log(` Parent: ${parent.name} (${parent.eventStatus})`);
1267
+ }
1268
+ }
1269
+ }
1270
+ ```
1271
+
1272
+ #### Response Shape
1273
+
1274
+ `getEvents()` returns `FluentEventLogResponse`:
1275
+
1276
+ ```typescript
1277
+ {
1278
+ start: 1, // Pagination offset
1279
+ count: 1000, // Results in this page
1280
+ hasMore: false, // More pages available?
1281
+ results: [ // Array of FluentEventLogItem
1282
+ {
1283
+ id: "e2cc5040-...", // UUID
1284
+ name: "BATCH_COMPLETE", // Event/ruleset name (can be null)
1285
+ type: "ORCHESTRATION_AUDIT",
1286
+ accountId: "MYACCOUNT",
1287
+ retailerId: "5", // String in response (not number)
1288
+ category: "BATCH",
1289
+ context: {
1290
+ sourceEvents: ["babc5f37-..."], // Parent event IDs for tracing
1291
+ entityType: "BATCH",
1292
+ entityId: "12",
1293
+ entityRef: "12",
1294
+ rootEntityType: "JOB",
1295
+ rootEntityId: "13",
1296
+ rootEntityRef: "13"
1297
+ },
1298
+ eventStatus: "COMPLETE",
1299
+ attributes: null, // null OR array of { name, value, type }
1300
+ source: null,
1301
+ generatedBy: "Rubix User",
1302
+ generatedOn: "2026-02-05T06:31:56.895+00:00"
1303
+ }
1304
+ ]
1305
+ }
1306
+ ```
1307
+
1308
+ `getEventById(id)` returns a single `FluentEventLogItem` (same shape as each item in `results`).
1309
+
1310
+ #### Query Parameters
1311
+
1312
+ All parameters are optional. Context filters use flat dot-notation keys:
1313
+
1314
+ | Parameter | Type | Description | Example |
1315
+ |-----------|------|-------------|---------|
1316
+ | `context.rootEntityType` | string | Root entity type | `'ORDER'`, `'JOB'`, `'LOCATION'` |
1317
+ | `context.rootEntityId` | string | Root entity ID | `'12345'` |
1318
+ | `context.rootEntityRef` | string | Root entity reference | `'ORD-12345'` |
1319
+ | `context.entityType` | string | Sub-entity type | `'FULFILMENT'`, `'BATCH'`, `'ARTICLE'` |
1320
+ | `context.entityId` | string | Sub-entity ID | `'67890'` |
1321
+ | `context.entityRef` | string | Sub-entity reference | `'FUL-67890'` |
1322
+ | `eventType` | string | Event type filter | `'ORCHESTRATION_AUDIT'` |
1323
+ | `eventStatus` | string | Status filter | `'FAILED'`, `'COMPLETE'` |
1324
+ | `name` | string | Event/ruleset name | `'BATCH_COMPLETE'` |
1325
+ | `category` | string | Category filter | `'ruleSet'`, `'ACTION'` |
1326
+ | `from` | string | Start date (UTC ISO 8601) | `'2026-02-01T00:00:00.000Z'` |
1327
+ | `to` | string | End date (UTC ISO 8601) | `'2026-02-15T23:59:59.999Z'` |
1328
+ | `retailerId` | string/number | Retailer filter (falls back to client config) | `'5'` |
1329
+ | `start` | number | Pagination offset (default: `0`) | `0` |
1330
+ | `count` | number | Results per page (default: `100`, max: `5000`) | `1000` |
1331
+
1332
+ #### Valid Values Reference
1333
+
1334
+ | Parameter | Valid Values |
1335
+ |-----------|-------------|
1336
+ | **eventType** | `ORCHESTRATION`, `ORCHESTRATION_AUDIT`, `API`, `INTEGRATION`, `SECURITY`, `GENERAL` |
1337
+ | **eventStatus** | `PENDING`, `SCHEDULED`, `NO_MATCH`, `SUCCESS`, `FAILED`, `COMPLETE` |
1338
+ | **category** | `snapshot`, `ruleSet`, `rule`, `ACTION`, `CUSTOM`, `exception`, `ORDER_WORKFLOW`, `BATCH` |
1339
+ | **rootEntityType** | `ORDER`, `LOCATION`, `FULFILMENT_OPTIONS`, `PRODUCT_CATALOGUE`, `INVENTORY_CATALOGUE`, `VIRTUAL_CATALOGUE`, `CONTROL_GROUP`, `RETURN_ORDER`, `BILLING_ACCOUNT`, `JOB` |
1340
+ | **entityType** | `ORDER`, `FULFILMENT`, `ARTICLE`, `CONSIGNMENT`, `LOCATION`, `WAVE`, `FULFILMENT_OPTIONS`, `FULFILMENT_PLAN`, `PRODUCT_CATALOGUE`, `CATEGORY`, `PRODUCT`, `INVENTORY_CATALOGUE`, `INVENTORY_POSITION`, `INVENTORY_QUANTITY`, `VIRTUAL_CATALOGUE`, `VIRTUAL_POSITION`, `CONTROL_GROUP`, `CONTROL`, `RETURN_ORDER`, `RETURN_FULFILMENT`, `BILLING_ACCOUNT`, `CREDIT_MEMO`, `BATCH` |
1341
+
1342
+ #### Common Use Cases
1343
+
1344
+ ```typescript
1345
+ // 1. Find failed events in the last 24 hours
1346
+ const failedEvents = await client.getEvents({
1347
+ eventStatus: 'FAILED',
1348
+ from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
1349
+ to: new Date().toISOString(),
1350
+ count: 1000,
1351
+ });
1352
+
1353
+ // 2. Track batch job processing (all events for a specific job)
1354
+ const batchEvents = await client.getEvents({
1355
+ 'context.rootEntityType': 'JOB',
1356
+ 'context.rootEntityId': '13',
1357
+ eventType: 'ORCHESTRATION_AUDIT',
1358
+ count: 500,
1359
+ });
1360
+
1361
+ // 3. Paginate through large result sets
1362
+ let start = 0;
1363
+ const allEvents: any[] = [];
1364
+ let hasMore = true;
1365
+
1366
+ while (hasMore) {
1367
+ const page = await client.getEvents({
1368
+ 'context.rootEntityType': 'ORDER',
1369
+ start,
1370
+ count: 1000,
1371
+ });
1372
+ allEvents.push(...page.results);
1373
+ hasMore = page.hasMore;
1374
+ start += page.count;
1375
+ }
1376
+ console.log(`Total events collected: ${allEvents.length}`);
1377
+
1378
+ // 4. Extract performance timing from event attributes
1379
+ const auditEvents = await client.getEvents({
1380
+ eventType: 'ORCHESTRATION_AUDIT',
1381
+ 'context.rootEntityType': 'JOB',
1382
+ count: 100,
1383
+ });
1384
+
1385
+ for (const event of auditEvents.results) {
1386
+ if (Array.isArray(event.attributes)) {
1387
+ const startTimer = event.attributes.find(a => a.name === 'startTimer');
1388
+ const stopTimer = event.attributes.find(a => a.name === 'stopTimer');
1389
+ if (startTimer && stopTimer) {
1390
+ const duration = Number(stopTimer.value) - Number(startTimer.value);
1391
+ console.log(`${event.name}: ${duration}ms`);
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ // 5. Find events for a specific order by reference
1397
+ const orderEvents = await client.getEvents({
1398
+ 'context.rootEntityType': 'ORDER',
1399
+ 'context.rootEntityRef': 'HD_12345',
1400
+ eventType: 'ORCHESTRATION_AUDIT',
1401
+ });
1402
+ ```
1403
+
1404
+ #### When to Use Which Method
1405
+
1406
+ | Method | Use When | Returns | Example |
1407
+ |--------|----------|---------|---------|
1408
+ | `sendEvent()` | **Trigger** a workflow or business action | Event creation response | Product upsert, order cancel, location update |
1409
+ | `getEvents()` | **Search/filter** historical events | `{ results[], hasMore, start, count }` | Find failed events, audit batch processing |
1410
+ | `getEventById()` | **Get full details** for one event by ID | Single event object | Drill into context, attributes, trace sourceEvents |
1411
+
1412
+ #### Limitations & Operational Notes
1413
+
1414
+ | Constraint | Value | Notes |
1415
+ |------------|-------|-------|
1416
+ | **Max count per request** | `5000` | Use pagination (`start`/`count`/`hasMore`) for larger result sets |
1417
+ | **Time range (from)** | 4 months back max | API rejects queries older than ~4 months |
1418
+ | **Time range (to)** | 1 month forward max | API rejects future dates beyond ~1 month |
1419
+ | **Default time window** | Past 30 days | Applied when `from` is omitted |
1420
+ | **Date format** | UTC ISO 8601 | `YYYY-MM-DDTHH:mm:ss.SSSZ` |
1421
+ | **retailerId fallback** | Client config | If not in params, uses `client.config.retailerId` |
1422
+ | **attributes field** | Nullable | Can be `null` (not empty array) — always check before iterating |
1423
+ | **name field** | Nullable | Can be `null` for some system events |
1424
+ | **GraphQL** | Not available | Event queries use REST only — no GraphQL `events` root field exists |
1425
+
1426
+ **Key points:**
1427
+ - `getEvents()` and `getEventById()` are **read-only** — they do NOT trigger workflows
1428
+ - Empty `results: []` is valid (not an error) — your filter may simply match zero events
1429
+ - Always use bounded time ranges (`from`/`to`) for predictable performance
1430
+ - Use `eventType: 'ORCHESTRATION_AUDIT'` for workflow execution history
1431
+ - Use `context.sourceEvents` array on each event to trace parent event chains
1432
+ - The `attributes` field is `null` for most events; only rule/ruleset events populate it
1433
+ - `retailerId` in responses is a **string** (e.g., `"5"`) even though you can pass a number in params
1434
+
1435
+ **📚 Detailed Guide:** `docs/02-CORE-GUIDES/api-reference/event-api-input-output-reference.md`
1436
+
1198
1437
  ---
1199
1438
 
1200
1439
  ### Error Classification
@@ -1386,6 +1625,110 @@ const validation = await authService.validateApiKey(
1386
1625
 
1387
1626
  ---
1388
1627
 
1628
+ ## 🚀 Standalone Usage (Outside Versori)
1629
+
1630
+ **Use the SDK in your own TypeScript/Node.js applications** - not just in Versori workflows.
1631
+
1632
+ ### Quick Example: Custom REST API Calls
1633
+
1634
+ ```typescript
1635
+ import { createClient } from '@fluentcommerce/fc-connect-sdk';
1636
+
1637
+ // Create client with OAuth2 authentication
1638
+ const client = await createClient({
1639
+ config: {
1640
+ baseUrl: 'https://api.fluentcommerce.com',
1641
+ clientId: 'FLUENT_INTEGRATION',
1642
+ clientSecret: 'your-client-secret',
1643
+ username: 'your-username',
1644
+ password: 'your-password',
1645
+ retailerId: 'your-retailer-id',
1646
+ },
1647
+ });
1648
+
1649
+ // Call any REST endpoint (authentication handled automatically)
1650
+ const response = await client.request('/orchestration/rest/v1/plugin', {
1651
+ method: 'GET',
1652
+ headers: {
1653
+ 'Content-Type': 'application/json',
1654
+ },
1655
+ });
1656
+
1657
+ console.log('Status:', response.status);
1658
+ console.log('Data:', response.data);
1659
+ ```
1660
+
1661
+ ### What's Handled Automatically
1662
+
1663
+ - ✅ **OAuth2 Authentication** - Token fetched and cached automatically
1664
+ - ✅ **Token Refresh** - Uses refresh_token when available, falls back to full auth
1665
+ - ✅ **401 Retry** - Automatically retries on 401 errors with fresh token
1666
+ - ✅ **Error Handling** - Retries 5xx errors with exponential backoff
1667
+ - ✅ **Any REST Endpoint** - Call any Fluent Commerce API endpoint
1668
+
1669
+ ### Complete Examples
1670
+
1671
+ - **📄 Full Example:** `examples/standalone-rest-api-call.ts` - Complete working example with GET, POST, query parameters
1672
+ - **📚 Documentation:** `examples/README-standalone-usage.md` - Detailed guide with all options
1673
+ - **✅ Verification:** `examples/VERIFICATION.md` - SDK implementation verification
1674
+
1675
+ ### Use Cases
1676
+
1677
+ - Custom admin tools and dashboards
1678
+ - Standalone scripts and automation
1679
+ - Integration with other systems
1680
+ - Testing and development tools
1681
+ - Any TypeScript/Node.js application
1682
+
1683
+ **📚 Learn More:** See `examples/README-standalone-usage.md` for complete documentation
1684
+
1685
+ ---
1686
+
1687
+ ## Security & Compliance
1688
+
1689
+ The SDK includes comprehensive security audit tools and SOC 2 compliance documentation.
1690
+
1691
+ ### Quick Security Check
1692
+
1693
+ ```bash
1694
+ # Run comprehensive security audit
1695
+ npm run security:check
1696
+
1697
+ # Fix vulnerabilities safely (with tests)
1698
+ npm run security:fix
1699
+
1700
+ # Advanced scanning with Snyk
1701
+ npm run security:snyk
1702
+ ```
1703
+
1704
+ ### Security Tools
1705
+
1706
+ - **Security Audit Script** - Automated vulnerability scanning (`security-audit/security-audit.cjs`)
1707
+ - **Safe Fix Script** - Fix vulnerabilities with automatic testing (`security-audit/safe-fix.cjs`)
1708
+ - **Snyk Integration** - Advanced vulnerability scanning with detailed reports
1709
+ - **GitHub Actions** - Automated weekly security scans
1710
+ - **Dependabot** - Automated dependency updates
1711
+
1712
+ ### Documentation
1713
+
1714
+ - **[Security Audit Quick Start](security-audit/QUICK-START.md)** - Get started with security checks
1715
+ - **[SOC 2 Compliance Guide](security-audit/SOC2-COMPLIANCE-GUIDE.md)** - Complete SOC 2 compliance requirements
1716
+ - **[Vulnerability Checking Guide](security-audit/VULNERABILITY-CHECKING.md)** - Detailed vulnerability management
1717
+ - **[Safe Fixing Guide](security-audit/SAFE-FIXING-GUIDE.md)** - How to fix vulnerabilities without breaking code
1718
+ - **[Advanced Scanning](security-audit/ADVANCED-SCANNING.md)** - Snyk and advanced security tools
1719
+ - **[What is the Audit?](security-audit/WHAT-IS-THE-AUDIT.md)** - Understanding security audits and risks
1720
+
1721
+ ### Current Security Status
1722
+
1723
+ ✅ **Production Code:** Secure - All production dependencies are secure
1724
+ ⚠️ **Remaining:** 3 moderate vulnerabilities in transitive dependencies (monitoring)
1725
+ ✅ **Tests:** All 3420 tests passing
1726
+ ✅ **Build:** Successful
1727
+
1728
+ See [security-audit/FIXES-APPLIED.md](security-audit/FIXES-APPLIED.md) for detailed security status.
1729
+
1730
+ ---
1731
+
1389
1732
  ## Common Pitfalls (and fixes)
1390
1733
 
1391
1734
  ### ⚠️ Critical Anti-Patterns
@@ -78,8 +78,9 @@ class FluentClient {
78
78
  paginationEnabled,
79
79
  direction: paginationVars.direction,
80
80
  });
81
+ const errorHandling = payload.errorHandling ?? 'throw';
81
82
  if (!paginationEnabled) {
82
- return this.executeSinglePage(payload);
83
+ return this.executeSinglePage(payload, errorHandling);
83
84
  }
84
85
  let direction = payload.pagination?.direction ?? paginationVars.direction;
85
86
  if (paginationVars.direction === 'none' && !payload.pagination?.direction) {
@@ -99,16 +100,18 @@ class FluentClient {
99
100
  abortSignal: payload.pagination?.abortSignal,
100
101
  onProgress: payload.pagination?.onProgress ?? (() => { }),
101
102
  onWarning: payload.pagination?.onWarning ?? ((msg) => this.log('warn', msg)),
103
+ errorHandling,
102
104
  };
103
105
  this.log('info', '[FluentClient:graphql] Auto-pagination enabled', {
104
106
  maxPages: config.maxPages,
105
107
  maxRecords: config.maxRecords,
106
108
  timeoutMs: config.timeoutMs,
107
109
  direction: config.direction,
110
+ errorHandling: config.errorHandling,
108
111
  });
109
112
  return this.executeWithAutoPagination(payload, paginationVars, config);
110
113
  }
111
- async executeSinglePage(payload) {
114
+ async executeSinglePage(payload, errorHandling = 'throw') {
112
115
  const operationName = payload.operationName || 'unnamed';
113
116
  try {
114
117
  const apiPayload = {
@@ -125,9 +128,21 @@ class FluentClient {
125
128
  body: apiPayload,
126
129
  });
127
130
  if (response.data.errors?.length) {
131
+ if (errorHandling === 'partial') {
132
+ this.log('warn', `[FluentClient:graphql] GraphQL operation "${operationName}" returned partial data with ${response.data.errors.length} error(s)`, {
133
+ operationName,
134
+ errorMessages: response.data.errors.map(e => e.message),
135
+ errorCount: response.data.errors.length,
136
+ hasData: !!response.data.data,
137
+ });
138
+ return {
139
+ ...response.data,
140
+ hasPartialData: true,
141
+ };
142
+ }
128
143
  this.log('error', `[FluentClient:graphql] GraphQL operation "${operationName}" returned ${response.data.errors.length} error(s)`, {
129
144
  operationName,
130
- errors: response.data.errors,
145
+ errorMessages: response.data.errors.map(e => e.message),
131
146
  errorCount: response.data.errors.length,
132
147
  });
133
148
  const firstError = response.data.errors[0];
@@ -152,6 +167,7 @@ class FluentClient {
152
167
  let lastCursor = null;
153
168
  let emptyPageCount = 0;
154
169
  const MAX_EMPTY_PAGES = FluentClient.DEFAULT_PAGINATION_CONFIG.maxEmptyPages;
170
+ const accumulatedErrors = [];
155
171
  while (true) {
156
172
  if (config.abortSignal?.aborted) {
157
173
  truncated = true;
@@ -172,17 +188,29 @@ class FluentClient {
172
188
  const response = await this.executeSinglePage({
173
189
  query: payload.query,
174
190
  variables: currentVariables,
175
- });
191
+ }, 'partial');
176
192
  lastExtensions = response.extensions;
177
193
  if (response.errors && response.errors.length > 0) {
178
- this.log('error', `[FluentClient:pagination] GraphQL errors during pagination (page ${pageNumber + 1}, ${totalRecords} records fetched so far)`, {
179
- errors: response.errors,
180
- pagesCompleted: pageNumber,
181
- totalRecordsBeforeError: totalRecords,
182
- errorCount: response.errors.length,
183
- });
184
- const firstError = response.errors[0];
185
- throw new ingestion_errors_1.GraphQLExecutionError(`GraphQL error during pagination (page ${pageNumber}): ${firstError.message || 'Unknown error'}`, response.errors, payload.query, currentVariables);
194
+ if (config.errorHandling === 'partial') {
195
+ this.log('warn', `[FluentClient:pagination] Page ${pageNumber + 1} returned partial data with ${response.errors.length} error(s)`, {
196
+ errorMessages: response.errors.map(e => e.message),
197
+ pagesCompleted: pageNumber,
198
+ totalRecordsSoFar: totalRecords,
199
+ errorCount: response.errors.length,
200
+ hasData: !!response.data,
201
+ });
202
+ accumulatedErrors.push(...response.errors);
203
+ }
204
+ else {
205
+ this.log('error', `[FluentClient:pagination] GraphQL errors during pagination (page ${pageNumber + 1}, ${totalRecords} records fetched so far)`, {
206
+ errorMessages: response.errors.map(e => e.message),
207
+ pagesCompleted: pageNumber,
208
+ totalRecordsBeforeError: totalRecords,
209
+ errorCount: response.errors.length,
210
+ });
211
+ const firstError = response.errors[0];
212
+ throw new ingestion_errors_1.GraphQLExecutionError(`GraphQL error during pagination (page ${pageNumber}): ${firstError.message || 'Unknown error'}`, response.errors, payload.query, currentVariables);
213
+ }
186
214
  }
187
215
  pageNumber++;
188
216
  const connection = (0, pagination_helpers_1.extractConnection)(response.data, config.connectionPath);
@@ -260,14 +288,17 @@ class FluentClient {
260
288
  await new Promise(resolve => setTimeout(resolve, config.delayMs));
261
289
  }
262
290
  }
263
- this.log('info', `[FluentClient:pagination] Pagination complete (${pageNumber} pages, ${totalRecords} records${truncated ? `, truncated: ${truncationReason}` : ''})`, {
291
+ const hasPartialErrors = accumulatedErrors.length > 0;
292
+ this.log('info', `[FluentClient:pagination] Pagination complete (${pageNumber} pages, ${totalRecords} records${truncated ? `, truncated: ${truncationReason}` : ''}${hasPartialErrors ? `, ${accumulatedErrors.length} errors` : ''})`, {
264
293
  totalPages: pageNumber,
265
294
  totalRecords,
266
295
  truncated,
267
296
  truncationReason,
268
297
  duration: Date.now() - startTime,
298
+ hasPartialErrors,
299
+ errorCount: accumulatedErrors.length,
269
300
  });
270
- return {
301
+ const response = {
271
302
  data: allData,
272
303
  extensions: {
273
304
  ...(lastExtensions || {}),
@@ -280,6 +311,11 @@ class FluentClient {
280
311
  },
281
312
  },
282
313
  };
314
+ if (hasPartialErrors) {
315
+ response.errors = accumulatedErrors;
316
+ response.hasPartialData = true;
317
+ }
318
+ return response;
283
319
  }
284
320
  mergeConnectionData(allData, newData, connection, direction) {
285
321
  const allConnection = (0, pagination_helpers_1.extractConnection)(allData);
@@ -477,6 +513,56 @@ class FluentClient {
477
513
  throw error;
478
514
  }
479
515
  }
516
+ async getEvents(params = {}) {
517
+ const queryString = this.buildEventQueryString(params);
518
+ const endpoint = queryString ? `/api/v4.1/event?${queryString}` : '/api/v4.1/event';
519
+ this.log('info', '[FluentClient:event] Searching event logs', {
520
+ endpoint,
521
+ filterCount: Object.keys(params).length,
522
+ hasDateRange: !!(params.from || params.to),
523
+ entityType: params['context.entityType'],
524
+ rootEntityType: params['context.rootEntityType'],
525
+ eventStatus: params.eventStatus,
526
+ });
527
+ try {
528
+ const response = await this.request(endpoint, {
529
+ method: 'GET',
530
+ });
531
+ this.log('info', '[FluentClient:event] Event search completed', {
532
+ resultCount: response.data.count,
533
+ hasMore: response.data.hasMore,
534
+ start: response.data.start,
535
+ });
536
+ return response.data;
537
+ }
538
+ catch (error) {
539
+ this.log('error', '[FluentClient:event] Failed to search events', error);
540
+ throw error;
541
+ }
542
+ }
543
+ async getEventById(eventId) {
544
+ if (!eventId) {
545
+ throw new types_1.FluentValidationError('eventId is required');
546
+ }
547
+ this.log('info', `[FluentClient:event] Getting event by ID: ${eventId}`, { eventId });
548
+ try {
549
+ const response = await this.request(`/api/v4.1/event/${eventId}`, {
550
+ method: 'GET',
551
+ });
552
+ this.log('info', `[FluentClient:event] Event retrieved: ${response.data.name}`, {
553
+ eventId: response.data.id,
554
+ name: response.data.name,
555
+ type: response.data.type,
556
+ eventStatus: response.data.eventStatus,
557
+ entityType: response.data.context?.entityType,
558
+ });
559
+ return response.data;
560
+ }
561
+ catch (error) {
562
+ this.log('error', `[FluentClient:event] Failed to get event ${eventId}`, error);
563
+ throw error;
564
+ }
565
+ }
480
566
  async validateWebhook(payload, signature, rawPayload) {
481
567
  this.log('info', `[FluentClient:webhook] Validating webhook for event "${payload.name || 'unknown'}"`, {
482
568
  hasSignature: !!signature,
@@ -556,6 +642,16 @@ class FluentClient {
556
642
  const result = await this.graphql(payload);
557
643
  return result.data;
558
644
  }
645
+ buildEventQueryString(params) {
646
+ const searchParams = new URLSearchParams();
647
+ for (const [key, value] of Object.entries(params)) {
648
+ if (value === undefined || value === null || value === '') {
649
+ continue;
650
+ }
651
+ searchParams.append(key, String(value));
652
+ }
653
+ return searchParams.toString();
654
+ }
559
655
  async request(endpoint, config = {}) {
560
656
  const isVersoriContext = this.context?.fetch && this.context?.fetch !== globalThis.fetch;
561
657
  this.log('debug', 'Request mode detection', {
@@ -378,7 +378,7 @@ class S3DataSource extends abstract_data_source_1.AbstractDataSource {
378
378
  return Buffer.from('PAR1\x00\x00\x00\x00PAR1', 'ascii');
379
379
  }
380
380
  try {
381
- const parquet = await Promise.resolve().then(() => __importStar(require('parquetjs')));
381
+ const parquet = await Promise.resolve().then(() => __importStar(require('@dsnp/parquetjs')));
382
382
  const ParquetWriter = parquet.ParquetWriter;
383
383
  const ParquetSchema = parquet.ParquetSchema;
384
384
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
@@ -361,7 +361,7 @@ class SftpDataSource extends abstract_data_source_1.AbstractDataSource {
361
361
  return Buffer.from('PAR1\x00\x00\x00\x00PAR1', 'ascii');
362
362
  }
363
363
  try {
364
- const parquet = await Promise.resolve().then(() => __importStar(require('parquetjs')));
364
+ const parquet = await Promise.resolve().then(() => __importStar(require('@dsnp/parquetjs')));
365
365
  const ParquetWriter = parquet.ParquetWriter;
366
366
  const ParquetSchema = parquet.ParquetSchema;
367
367
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
@@ -34,7 +34,7 @@ export { GraphQLTemplateGenerator } from './services/mapping/query/graphql-templ
34
34
  export { MappingError, ResolverError } from './errors';
35
35
  export type { NodeConfig, NodesConfig, FieldConfig, FieldsConfig, NodesContext, MappingContext, ResolverFunction, ResolverHelpers, ResolverContext, ResolversMap, MapResult, MapWithNodesResult, NodeValidationResult, MappingOptions, } from './services/mapping/types';
36
36
  export { CsvDelimiter, FileEncoding, JobStrategy, FileType, BatchAction, EntityType, AwsRegion, ValidationMode, ProcessingStatus, } from './types/enums';
37
- export type { FluentClientConfig, FluentBatchPayload, FluentInventoryBatchEntity, FluentInventoryBatchRequest, FluentBatchResponse, FluentBatchStatus, FluentJobPayload, FluentJobResponse, FluentJobMetadata, FluentJobStatus, FluentJobResults, FluentEvent, FluentEventMode, CreateEventOptions, GraphQLPayload, GraphQLResponse, PaginationConfig, PageInfo, PaginatedResponse, FluentWebhookPayload, WebhookRequestContext, GraphQLValidationResult, BatchConfiguration, DataSourceConfig, FileMetadata, Logger, StructuredLogger, LogContext, LoggerConfig, PerformanceMetrics, InventoryDataRecord, ParquetDataRecord, FieldMappingConfig, FieldMappingRule, ProcessingMetadata, AttributeValue, } from './types';
37
+ export type { FluentClientConfig, FluentBatchPayload, FluentInventoryBatchEntity, FluentInventoryBatchRequest, FluentBatchResponse, FluentBatchStatus, FluentJobPayload, FluentJobResponse, FluentJobMetadata, FluentJobStatus, FluentJobResults, FluentEvent, FluentEventMode, CreateEventOptions, FluentEventRootEntityType, FluentEventEntityType, FluentEventCategory, FluentEventType, FluentEventStatus, FluentEventQueryParamValue, FluentEventQueryParams, FluentEventLogAttribute, FluentEventLogContext, FluentEventLogItem, FluentEventLogResponse, GraphQLPayload, GraphQLResponse, GraphQLErrorMode, GraphQLError, PaginationConfig, PageInfo, PaginatedResponse, FluentWebhookPayload, WebhookRequestContext, GraphQLValidationResult, BatchConfiguration, DataSourceConfig, FileMetadata, Logger, StructuredLogger, LogContext, LoggerConfig, PerformanceMetrics, InventoryDataRecord, ParquetDataRecord, FieldMappingConfig, FieldMappingRule, ProcessingMetadata, AttributeValue, } from './types';
38
38
  export type { VersoriContext, WebhookContext, DirectContext } from './types';
39
39
  export { S3SDKTester, S3PresignedTester, S3ComparisonTester, FluentConnectionTester, type TestResult, type FluentTestConfig, type S3TestConfig, } from './testing/index';
40
40
  export { S3Service, S3ServiceError, type S3ServiceConfig, type S3Config, type S3Object, type ListOptions, type PutOptions, type CopyOptions, type S3Location, } from './services/s3/index';
@@ -1,5 +1,5 @@
1
1
  import type { FluentClient } from '../../clients/fluent-client';
2
- import type { Logger } from '../../types';
2
+ import type { Logger, GraphQLErrorMode, GraphQLError } from '../../types';
3
3
  export interface ExtractionOptions<T = any> {
4
4
  query: string;
5
5
  resultPath: string;
@@ -10,6 +10,8 @@ export interface ExtractionOptions<T = any> {
10
10
  direction?: 'forward' | 'backward';
11
11
  timeout?: number;
12
12
  validateItem?: (item: T) => boolean;
13
+ operationName?: string;
14
+ errorHandling?: GraphQLErrorMode;
13
15
  }
14
16
  export interface ExtractionStats {
15
17
  totalRecords: number;
@@ -20,6 +22,7 @@ export interface ExtractionStats {
20
22
  validRecords?: number;
21
23
  invalidRecords?: number;
22
24
  direction?: 'forward' | 'backward';
25
+ partialErrors?: GraphQLError[];
23
26
  }
24
27
  export interface ExtractionError {
25
28
  type: 'graphql' | 'network' | 'validation' | 'parsing';