@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
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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' &&
|
|
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
|
-
|
|
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 (
|
|
95
|
-
|
|
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?:
|
|
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,
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|