@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
@@ -20,13 +20,17 @@ class ExtractionOrchestrator {
20
20
  const pageSize = options.pageSize || this.DEFAULT_PAGE_SIZE;
21
21
  const timeout = options.timeout || this.DEFAULT_TIMEOUT;
22
22
  const direction = options.direction || 'forward';
23
+ const errorHandling = options.errorHandling || 'throw';
24
+ const accumulatedGraphQLErrors = [];
23
25
  this.logger.info('Starting data extraction', {
26
+ operationName: options.operationName || 'unnamed',
24
27
  resultPath: options.resultPath,
25
28
  pageSize,
26
29
  maxRecords: options.maxRecords,
27
30
  maxPages: options.maxPages,
28
31
  timeout,
29
32
  direction,
33
+ errorHandling,
30
34
  });
31
35
  try {
32
36
  const variables = {
@@ -36,16 +40,39 @@ class ExtractionOrchestrator {
36
40
  const response = await this.executeWithTimeout(this.client.graphql({
37
41
  query: options.query,
38
42
  variables,
43
+ ...(options.operationName && { operationName: options.operationName }),
44
+ errorHandling,
39
45
  }), timeout);
40
46
  if (response.errors && response.errors.length > 0) {
41
- const graphqlError = new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(', ')}`);
42
- this.logger.error('GraphQL query returned errors', graphqlError, {
43
- errors: response.errors,
47
+ if (errorHandling === 'partial') {
48
+ this.logger.warn('GraphQL query returned partial data with errors (page 1)', {
49
+ errors: response.errors,
50
+ errorCount: response.errors.length,
51
+ hasData: !!response.data,
52
+ });
53
+ accumulatedGraphQLErrors.push(...response.errors);
54
+ }
55
+ else {
56
+ const graphqlError = new Error(`GraphQL errors: ${response.errors.map((e) => e.message).join(', ')}`);
57
+ this.logger.error('GraphQL query returned errors', graphqlError, {
58
+ errors: response.errors,
59
+ errorCount: response.errors.length,
60
+ });
61
+ throw graphqlError;
62
+ }
63
+ }
64
+ let records = [];
65
+ if (response.data) {
66
+ records = this.extractFromPath(response.data, options.resultPath);
67
+ }
68
+ else if (errorHandling === 'partial' && response.errors && response.errors.length > 0) {
69
+ this.logger.warn('GraphQL response has errors but no data (page 1), returning empty records', {
44
70
  errorCount: response.errors.length,
45
71
  });
46
- throw graphqlError;
47
72
  }
48
- let records = this.extractFromPath(response.data, options.resultPath);
73
+ else if (!response.data) {
74
+ records = this.extractFromPath(response.data, options.resultPath);
75
+ }
49
76
  if (options.maxRecords && records.length > options.maxRecords) {
50
77
  records = records.slice(0, options.maxRecords);
51
78
  }
@@ -59,14 +86,21 @@ class ExtractionOrchestrator {
59
86
  const rootPath = edgesIdx >= 0 ? pathParts.slice(0, edgesIdx).join('.') : pathParts.slice(0, -1).join('.');
60
87
  const pageInfoPath = rootPath ? `${rootPath}.pageInfo` : 'pageInfo';
61
88
  const edgesPath = edgesIdx >= 0 ? `${rootPath}.edges` : options.resultPath;
62
- const firstPageInfo = this.safeGet(response.data, pageInfoPath) || {};
89
+ const firstPageInfo = response.data ? this.safeGet(response.data, pageInfoPath) || {} : {};
63
90
  let hasMore = direction === 'forward'
64
91
  ? Boolean(firstPageInfo?.hasNextPage)
65
92
  : Boolean(firstPageInfo?.hasPreviousPage);
66
- if (direction === 'backward' && firstPageInfo?.hasPreviousPage === undefined) {
93
+ if (direction === 'backward' &&
94
+ response.data &&
95
+ firstPageInfo?.hasPreviousPage === undefined) {
67
96
  throw new Error('Backward pagination requested but pageInfo.hasPreviousPage is missing from query response');
68
97
  }
69
- let cursor = this.pickCursor(response.data, edgesPath, direction);
98
+ if (!response.data && errorHandling === 'partial' && accumulatedGraphQLErrors.length > 0) {
99
+ hasMore = false;
100
+ }
101
+ let cursor = response.data
102
+ ? this.pickCursor(response.data, edgesPath, direction)
103
+ : undefined;
70
104
  while (hasMore) {
71
105
  if (options.maxPages && totalPages >= options.maxPages) {
72
106
  truncated = true;
@@ -90,11 +124,40 @@ class ExtractionOrchestrator {
90
124
  const nextPage = await this.executeWithTimeout(this.client.graphql({
91
125
  query: options.query,
92
126
  variables: nextVariables,
127
+ ...(options.operationName && { operationName: options.operationName }),
128
+ errorHandling,
93
129
  }), timeout);
94
- if (!nextPage || nextPage.errors) {
95
- throw new Error(`GraphQL extraction failed on subsequent page${nextPage?.errors ? `: ${JSON.stringify(nextPage.errors)}` : ''}`);
130
+ if (nextPage?.errors && nextPage.errors.length > 0) {
131
+ if (errorHandling === 'partial') {
132
+ this.logger.warn(`GraphQL query returned partial data with errors (page ${totalPages + 1})`, {
133
+ errors: nextPage.errors,
134
+ errorCount: nextPage.errors.length,
135
+ hasData: !!nextPage.data,
136
+ });
137
+ accumulatedGraphQLErrors.push(...nextPage.errors);
138
+ }
139
+ else {
140
+ throw new Error(`GraphQL extraction failed on subsequent page: ${JSON.stringify(nextPage.errors)}`);
141
+ }
142
+ }
143
+ if (!nextPage) {
144
+ throw new Error('GraphQL extraction failed on subsequent page: no response');
145
+ }
146
+ let pageRecords = [];
147
+ if (nextPage.data) {
148
+ pageRecords = this.extractFromPath(nextPage.data, options.resultPath);
149
+ }
150
+ else if (errorHandling === 'partial' && nextPage.errors && nextPage.errors.length > 0) {
151
+ this.logger.warn(`GraphQL response has errors but no data (page ${totalPages + 1}), stopping pagination`, {
152
+ errorCount: nextPage.errors.length,
153
+ });
154
+ hasMore = false;
155
+ cursor = undefined;
156
+ continue;
157
+ }
158
+ else if (!nextPage.data) {
159
+ pageRecords = this.extractFromPath(nextPage.data, options.resultPath);
96
160
  }
97
- let pageRecords = this.extractFromPath(nextPage.data, options.resultPath);
98
161
  this.validateRecords(pageRecords, options.validateItem);
99
162
  if (options.maxRecords && records.length + pageRecords.length > options.maxRecords) {
100
163
  const remaining = options.maxRecords - records.length;
@@ -128,6 +191,7 @@ class ExtractionOrchestrator {
128
191
  validRecords: finalValidation.validRecords,
129
192
  invalidRecords: finalValidation.invalidRecords,
130
193
  direction,
194
+ ...(accumulatedGraphQLErrors.length > 0 && { partialErrors: accumulatedGraphQLErrors }),
131
195
  };
132
196
  this.logger.info('Data extraction completed', {
133
197
  totalRecords: stats.totalRecords,
@@ -136,7 +200,16 @@ class ExtractionOrchestrator {
136
200
  truncated: stats.truncated,
137
201
  truncationReason: stats.truncationReason,
138
202
  direction,
203
+ hasPartialErrors: accumulatedGraphQLErrors.length > 0,
204
+ partialErrorCount: accumulatedGraphQLErrors.length,
139
205
  });
206
+ if (accumulatedGraphQLErrors.length > 0) {
207
+ this.logger.warn('Extraction completed with partial errors', {
208
+ errorCount: accumulatedGraphQLErrors.length,
209
+ recordsExtracted: stats.totalRecords,
210
+ message: 'Some GraphQL errors occurred but extraction continued with available data.',
211
+ });
212
+ }
140
213
  if (stats.truncated) {
141
214
  this.logger.warn('Extraction was truncated', {
142
215
  reason: stats.truncationReason,
@@ -95,23 +95,91 @@ export interface FluentEvent {
95
95
  export interface CreateEventOptions {
96
96
  mode?: FluentEventMode;
97
97
  }
98
+ export type FluentEventRootEntityType = 'ORDER' | 'LOCATION' | 'FULFILMENT_OPTIONS' | 'PRODUCT_CATALOGUE' | 'INVENTORY_CATALOGUE' | 'VIRTUAL_CATALOGUE' | 'CONTROL_GROUP' | 'RETURN_ORDER' | 'BILLING_ACCOUNT' | 'JOB';
99
+ export type FluentEventEntityType = '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';
100
+ export type FluentEventCategory = 'snapshot' | 'ruleSet' | 'rule' | 'ACTION' | 'CUSTOM' | 'exception' | 'ORDER_WORKFLOW';
101
+ export type FluentEventType = 'ORCHESTRATION' | 'ORCHESTRATION_AUDIT' | 'API' | 'INTEGRATION' | 'SECURITY' | 'GENERAL';
102
+ export type FluentEventStatus = 'PENDING' | 'SCHEDULED' | 'NO_MATCH' | 'SUCCESS' | 'FAILED' | 'COMPLETE';
103
+ export type FluentEventQueryParamValue = string | number | boolean;
104
+ export interface FluentEventQueryParams {
105
+ start?: number;
106
+ count?: number;
107
+ from?: string;
108
+ to?: string;
109
+ name?: string;
110
+ category?: FluentEventCategory | string;
111
+ retailerId?: string | number;
112
+ eventType?: FluentEventType | string;
113
+ eventStatus?: FluentEventStatus | string;
114
+ eventId?: string;
115
+ 'context.rootEntityType'?: FluentEventRootEntityType | string;
116
+ 'context.rootEntityId'?: string | number;
117
+ 'context.rootEntityRef'?: string;
118
+ 'context.entityType'?: FluentEventEntityType | string;
119
+ 'context.entityId'?: string | number;
120
+ 'context.entityRef'?: string;
121
+ 'context.sourceEvents'?: string;
122
+ [key: string]: FluentEventQueryParamValue | undefined;
123
+ }
124
+ export interface FluentEventLogContext {
125
+ sourceEvents?: string[];
126
+ entityType?: FluentEventEntityType | string;
127
+ entityId?: string;
128
+ entityRef?: string;
129
+ rootEntityType?: FluentEventRootEntityType | string;
130
+ rootEntityId?: string;
131
+ rootEntityRef?: string;
132
+ [key: string]: JsonValue | undefined;
133
+ }
134
+ export interface FluentEventLogAttribute {
135
+ name: string;
136
+ value: string;
137
+ type?: string;
138
+ [key: string]: unknown;
139
+ }
140
+ export interface FluentEventLogItem {
141
+ id: string;
142
+ name: string;
143
+ type?: FluentEventType | string;
144
+ accountId?: string;
145
+ retailerId?: string;
146
+ category?: FluentEventCategory | string;
147
+ context?: FluentEventLogContext;
148
+ eventStatus?: FluentEventStatus | string;
149
+ attributes?: FluentEventLogAttribute[] | Record<string, JsonValue> | null;
150
+ source?: string | null;
151
+ generatedBy?: string;
152
+ generatedOn?: string;
153
+ [key: string]: unknown;
154
+ }
155
+ export interface FluentEventLogResponse {
156
+ start: number;
157
+ count: number;
158
+ hasMore: boolean;
159
+ results: FluentEventLogItem[];
160
+ [key: string]: unknown;
161
+ }
162
+ export type GraphQLErrorMode = 'throw' | 'partial';
98
163
  export interface GraphQLPayload<T = Record<string, JsonValue>> {
99
164
  query: string;
100
165
  variables?: T;
101
166
  operationName?: string;
102
167
  pagination?: PaginationConfig;
168
+ errorHandling?: GraphQLErrorMode;
169
+ }
170
+ export interface GraphQLError {
171
+ message: string;
172
+ locations?: Array<{
173
+ line: number;
174
+ column: number;
175
+ }>;
176
+ path?: (string | number)[];
177
+ extensions?: Record<string, JsonValue>;
178
+ [key: string]: unknown;
103
179
  }
104
180
  export interface GraphQLResponse<T = JsonValue> {
105
181
  data?: T;
106
- errors?: Array<{
107
- message: string;
108
- locations?: Array<{
109
- line: number;
110
- column: number;
111
- }>;
112
- path?: (string | number)[];
113
- extensions?: Record<string, JsonValue>;
114
- }>;
182
+ errors?: GraphQLError[];
115
183
  extensions?: Record<string, unknown> & {
116
184
  autoPagination?: {
117
185
  totalPages: number;
@@ -121,6 +189,7 @@ export interface GraphQLResponse<T = JsonValue> {
121
189
  direction?: 'forward' | 'backward';
122
190
  };
123
191
  };
192
+ hasPartialData?: boolean;
124
193
  }
125
194
  export interface FluentInventoryBatchEntity {
126
195
  locationRef: string;
@@ -359,7 +428,7 @@ export interface Logger {
359
428
  debug(message: string, meta?: Record<string, unknown> | undefined): void;
360
429
  info(message: string, meta?: Record<string, unknown> | undefined): void;
361
430
  warn(message: string, meta?: Record<string, unknown> | undefined): void;
362
- error(message: string, error?: Error, meta?: Record<string, unknown> | undefined): void;
431
+ error(message: string, errorOrMeta?: unknown, meta?: Record<string, unknown>): void;
363
432
  }
364
433
  export interface LogContext {
365
434
  [key: string]: MetadataValue;
@@ -1,4 +1,4 @@
1
- import type { FluentEvent, GraphQLPayload, GraphQLResponse, FluentEventMode, JsonValue, FluentBatchPayload, FluentBatchResponse, FluentBatchStatus, FluentJobPayload, FluentJobResponse, FluentJobStatus, FluentJobResults } from '../types';
1
+ import type { FluentEvent, GraphQLPayload, GraphQLResponse, FluentEventMode, FluentEventLogResponse, FluentEventLogItem, FluentEventQueryParams, JsonValue, FluentBatchPayload, FluentBatchResponse, FluentBatchStatus, FluentJobPayload, FluentJobResponse, FluentJobStatus, FluentJobResults } from '../types';
2
2
  import type { FluentWebhookPayload, EventResponse } from '../types';
3
3
  export declare class FluentVersoriClient {
4
4
  private ctx;
@@ -13,6 +13,8 @@ export declare class FluentVersoriClient {
13
13
  private executeSinglePage;
14
14
  private executeWithAutoPagination;
15
15
  sendEvent(event: FluentEvent, mode?: FluentEventMode): Promise<EventResponse>;
16
+ getEvents(params?: FluentEventQueryParams): Promise<FluentEventLogResponse>;
17
+ getEventById(eventId: string): Promise<FluentEventLogItem>;
16
18
  createJob(payload: FluentJobPayload): Promise<FluentJobResponse>;
17
19
  sendBatch(jobId: string, payload: FluentBatchPayload): Promise<FluentBatchResponse>;
18
20
  getJobStatus(jobId: string): Promise<FluentJobStatus>;
@@ -20,6 +22,7 @@ export declare class FluentVersoriClient {
20
22
  getJobResults(jobId: string): Promise<FluentJobResults>;
21
23
  query<T = JsonValue>(queryOrPayload: string | GraphQLPayload, variables?: Record<string, any>): Promise<T | undefined>;
22
24
  mutate<T = JsonValue>(mutationOrPayload: string | GraphQLPayload, variables?: Record<string, any>): Promise<T | undefined>;
25
+ private buildEventQueryString;
23
26
  private throwApiError;
24
27
  private fetchWithRetry;
25
28
  validateWebhook(payload: FluentWebhookPayload, signature?: string, rawPayload?: string, publicKey?: string): Promise<boolean>;
@@ -83,8 +83,9 @@ class FluentVersoriClient {
83
83
  });
84
84
  const paginationVars = (0, pagination_helpers_1.detectPaginationVariables)(payload.query);
85
85
  const paginationEnabled = !isMutation && (payload.pagination?.enabled ?? paginationVars.hasPagination);
86
+ const errorHandling = payload.errorHandling ?? 'throw';
86
87
  if (!paginationEnabled) {
87
- return this.executeSinglePage(payload);
88
+ return this.executeSinglePage(payload, errorHandling);
88
89
  }
89
90
  const direction = payload.pagination?.direction ??
90
91
  paginationVars.direction ??
@@ -100,10 +101,11 @@ class FluentVersoriClient {
100
101
  abortSignal: payload.pagination?.abortSignal,
101
102
  onProgress: payload.pagination?.onProgress ?? (() => { }),
102
103
  onWarning: payload.pagination?.onWarning ?? ((msg) => this.ctx.log?.warn?.(msg)),
104
+ errorHandling,
103
105
  };
104
106
  return this.executeWithAutoPagination(payload, paginationVars, config);
105
107
  }
106
- async executeSinglePage(payload) {
108
+ async executeSinglePage(payload, errorHandling = 'throw') {
107
109
  const operationName = payload.operationName || 'unnamed';
108
110
  try {
109
111
  const apiPayload = {
@@ -142,6 +144,18 @@ class FluentVersoriClient {
142
144
  throw new Error('Failed to parse GraphQL response');
143
145
  }
144
146
  if (parsedResponse.errors && parsedResponse.errors.length > 0) {
147
+ if (errorHandling === 'partial') {
148
+ this.ctx.log.warn(`[fc-connect-sdk:graphql] GraphQL operation "${operationName}" returned partial data with ${parsedResponse.errors.length} error(s)`, {
149
+ operationName,
150
+ errors: parsedResponse.errors,
151
+ errorCount: parsedResponse.errors.length,
152
+ hasData: !!parsedResponse.data,
153
+ });
154
+ return {
155
+ ...parsedResponse,
156
+ hasPartialData: true,
157
+ };
158
+ }
145
159
  this.ctx.log.error(`[fc-connect-sdk:graphql] GraphQL operation "${operationName}" returned ${parsedResponse.errors.length} error(s)`, {
146
160
  operationName,
147
161
  errors: parsedResponse.errors,
@@ -178,6 +192,7 @@ class FluentVersoriClient {
178
192
  let currentVariables = { ...(payload.variables || {}) };
179
193
  let lastCursor = null;
180
194
  let emptyPageCount = 0;
195
+ const accumulatedErrors = [];
181
196
  while (true) {
182
197
  if (config.abortSignal?.aborted) {
183
198
  truncated = true;
@@ -200,17 +215,29 @@ class FluentVersoriClient {
200
215
  const response = await this.executeSinglePage({
201
216
  query: payload.query,
202
217
  variables: currentVariables,
203
- });
218
+ }, 'partial');
204
219
  lastExtensions = response.extensions;
205
220
  if (response.errors && response.errors.length > 0) {
206
- this.ctx.log.error(`[fc-connect-sdk:pagination] GraphQL errors during pagination (page ${pageNumber + 1}, ${totalRecords} records fetched so far)`, {
207
- errors: response.errors,
208
- pagesCompleted: pageNumber,
209
- totalRecordsBeforeError: totalRecords,
210
- errorCount: response.errors.length,
211
- });
212
- const firstError = response.errors[0];
213
- throw new ingestion_errors_1.GraphQLExecutionError(`GraphQL error during pagination (page ${pageNumber}): ${firstError.message || 'Unknown error'}`, response.errors, payload.query, currentVariables);
221
+ if (config.errorHandling === 'partial') {
222
+ this.ctx.log.warn(`[fc-connect-sdk:pagination] Page ${pageNumber + 1} returned partial data with ${response.errors.length} error(s)`, {
223
+ errors: response.errors,
224
+ pagesCompleted: pageNumber,
225
+ totalRecordsSoFar: totalRecords,
226
+ errorCount: response.errors.length,
227
+ hasData: !!response.data,
228
+ });
229
+ accumulatedErrors.push(...response.errors);
230
+ }
231
+ else {
232
+ this.ctx.log.error(`[fc-connect-sdk:pagination] GraphQL errors during pagination (page ${pageNumber + 1}, ${totalRecords} records fetched so far)`, {
233
+ errors: response.errors,
234
+ pagesCompleted: pageNumber,
235
+ totalRecordsBeforeError: totalRecords,
236
+ errorCount: response.errors.length,
237
+ });
238
+ const firstError = response.errors[0];
239
+ throw new ingestion_errors_1.GraphQLExecutionError(`GraphQL error during pagination (page ${pageNumber}): ${firstError.message || 'Unknown error'}`, response.errors, payload.query, currentVariables);
240
+ }
214
241
  }
215
242
  pageNumber++;
216
243
  const connection = (0, pagination_helpers_1.extractConnection)(response.data, config.connectionPath);
@@ -313,14 +340,17 @@ class FluentVersoriClient {
313
340
  await new Promise(resolve => setTimeout(resolve, config.delayMs));
314
341
  }
315
342
  }
316
- this.ctx.log?.info?.(`[fc-connect-sdk:pagination] Pagination complete (${pageNumber} pages, ${totalRecords} records${truncated ? `, truncated: ${truncationReason}` : ''})`, {
343
+ const hasPartialErrors = accumulatedErrors.length > 0;
344
+ this.ctx.log?.info?.(`[fc-connect-sdk:pagination] Pagination complete (${pageNumber} pages, ${totalRecords} records${truncated ? `, truncated: ${truncationReason}` : ''}${hasPartialErrors ? `, ${accumulatedErrors.length} errors` : ''})`, {
317
345
  totalPages: pageNumber,
318
346
  totalRecords,
319
347
  truncated,
320
348
  truncationReason,
321
349
  duration: Date.now() - startTime,
350
+ hasPartialErrors,
351
+ errorCount: accumulatedErrors.length,
322
352
  });
323
- return {
353
+ const response = {
324
354
  data: allData,
325
355
  extensions: {
326
356
  ...(lastExtensions || {}),
@@ -333,6 +363,11 @@ class FluentVersoriClient {
333
363
  },
334
364
  },
335
365
  };
366
+ if (hasPartialErrors) {
367
+ response.errors = accumulatedErrors;
368
+ response.hasPartialData = true;
369
+ }
370
+ return response;
336
371
  }
337
372
  async sendEvent(event, mode = 'async') {
338
373
  this.ctx.log.info(`[fc-connect-sdk:event] Sending event "${event.name}" (${event.entityType}:${event.entityRef}, mode: ${mode})`, {
@@ -392,6 +427,79 @@ class FluentVersoriClient {
392
427
  return { success: true, statusCode: response.status, message: text };
393
428
  }
394
429
  }
430
+ async getEvents(params = {}) {
431
+ const query = this.buildEventQueryString(params);
432
+ const endpoint = query ? `/api/v4.1/event?${query}` : '/api/v4.1/event';
433
+ this.ctx.log.info('[fc-connect-sdk:event] Searching event logs', {
434
+ endpoint,
435
+ filterCount: Object.keys(params).length,
436
+ hasDateRange: !!(params.from || params.to),
437
+ entityType: params['context.entityType'],
438
+ rootEntityType: params['context.rootEntityType'],
439
+ eventStatus: params.eventStatus,
440
+ });
441
+ const response = await this.fetchWithRetry(endpoint, {
442
+ method: 'GET',
443
+ headers: {
444
+ 'Content-Type': 'application/json',
445
+ },
446
+ });
447
+ const text = response.text;
448
+ if (!response.ok) {
449
+ this.ctx.log.error('[fc-connect-sdk:event] Failed to search events', {
450
+ status: response.status,
451
+ body: text,
452
+ });
453
+ this.throwApiError('Get events failed', response.status, text);
454
+ }
455
+ try {
456
+ const result = JSON.parse(text);
457
+ this.ctx.log.info('[fc-connect-sdk:event] Event search completed', {
458
+ resultCount: result.count,
459
+ hasMore: result.hasMore,
460
+ start: result.start,
461
+ });
462
+ return result;
463
+ }
464
+ catch {
465
+ throw new types_1.FluentAPIError('Failed to parse event log response', response.status, text);
466
+ }
467
+ }
468
+ async getEventById(eventId) {
469
+ if (!eventId) {
470
+ throw new types_1.FluentValidationError('eventId is required');
471
+ }
472
+ this.ctx.log.info(`[fc-connect-sdk:event] Getting event by ID: ${eventId}`, { eventId });
473
+ const response = await this.fetchWithRetry(`/api/v4.1/event/${eventId}`, {
474
+ method: 'GET',
475
+ headers: {
476
+ 'Content-Type': 'application/json',
477
+ },
478
+ });
479
+ const text = response.text;
480
+ if (!response.ok) {
481
+ this.ctx.log.error(`[fc-connect-sdk:event] Failed to get event ${eventId}`, {
482
+ eventId,
483
+ status: response.status,
484
+ body: text,
485
+ });
486
+ this.throwApiError(`Get event ${eventId} failed`, response.status, text);
487
+ }
488
+ try {
489
+ const event = JSON.parse(text);
490
+ this.ctx.log.info(`[fc-connect-sdk:event] Event retrieved: ${event.name}`, {
491
+ eventId: event.id,
492
+ name: event.name,
493
+ type: event.type,
494
+ eventStatus: event.eventStatus,
495
+ entityType: event.context?.entityType,
496
+ });
497
+ return event;
498
+ }
499
+ catch {
500
+ throw new types_1.FluentAPIError('Failed to parse event response', response.status, text);
501
+ }
502
+ }
395
503
  async createJob(payload) {
396
504
  this.ctx.log.info(`[fc-connect-sdk:job] Creating job "${payload.name}"`, {
397
505
  name: payload.name,
@@ -539,6 +647,16 @@ class FluentVersoriClient {
539
647
  const result = await this.graphql(payload);
540
648
  return result.data;
541
649
  }
650
+ buildEventQueryString(params) {
651
+ const searchParams = new URLSearchParams();
652
+ for (const [key, value] of Object.entries(params)) {
653
+ if (value === undefined || value === null || value === '') {
654
+ continue;
655
+ }
656
+ searchParams.append(key, String(value));
657
+ }
658
+ return searchParams.toString();
659
+ }
542
660
  throwApiError(message, status, responseText) {
543
661
  if (status === 401) {
544
662
  throw new types_1.AuthenticationError(`${message}: ${responseText}`, { response: responseText });