@machinemetrics/mm-erp-sdk 0.1.1-beta.5 → 0.1.2
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/dist/{hashed-cache-manager-DkDox9wX.js → hashed-cache-manager-Ci59eC75.js} +2 -7
- package/dist/hashed-cache-manager-Ci59eC75.js.map +1 -0
- package/dist/{index-Cn9ccxOO.js → index-CXbOvFyf.js} +3 -1
- package/dist/{index-Cn9ccxOO.js.map → index-CXbOvFyf.js.map} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mm-erp-sdk.js +138 -52
- package/dist/mm-erp-sdk.js.map +1 -1
- package/dist/services/data-sync-service/configuration-manager.d.ts +0 -1
- package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +6 -3
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.js +6 -4
- package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
- package/dist/services/data-sync-service/jobs/run-migrations.js +7 -1
- package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
- package/dist/services/erp-api-services/graphql/graphql-service.d.ts +5 -0
- package/dist/services/erp-api-services/graphql/graphql-service.d.ts.map +1 -1
- package/dist/services/erp-api-services/rest/rest-api-service.d.ts +5 -0
- package/dist/services/erp-api-services/rest/rest-api-service.d.ts.map +1 -1
- package/dist/services/mm-api-service/mm-api-service.d.ts +5 -0
- package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
- package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts +24 -8
- package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts.map +1 -1
- package/dist/services/sql-server-erp-service/internal/sql-server-config.d.ts +3 -0
- package/dist/services/sql-server-erp-service/internal/sql-server-config.d.ts.map +1 -1
- package/dist/utils/http-client.d.ts +2 -1
- package/dist/utils/http-client.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/error-processor.d.ts +5 -5
- package/dist/utils/standard-process-drivers/error-processor.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +10 -9
- package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +6 -2
- package/src/services/data-sync-service/configuration-manager.ts +0 -9
- package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +4 -1
- package/src/services/data-sync-service/jobs/from-erp.ts +5 -3
- package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +3 -1
- package/src/services/data-sync-service/jobs/run-migrations.ts +7 -1
- package/src/services/erp-api-services/graphql/graphql-service.ts +8 -0
- package/src/services/erp-api-services/rest/rest-api-service.ts +8 -0
- package/src/services/mm-api-service/mm-api-service.ts +11 -2
- package/src/services/mm-api-service/types/mm-response-interfaces.ts +30 -14
- package/src/services/sql-server-erp-service/internal/sql-server-config.ts +1 -0
- package/src/utils/http-client.ts +111 -41
- package/src/utils/index.ts +6 -0
- package/src/utils/standard-process-drivers/error-processor.ts +11 -11
- package/src/utils/standard-process-drivers/standard-process-drivers.ts +10 -9
- package/dist/hashed-cache-manager-DkDox9wX.js.map +0 -1
|
@@ -25,13 +25,16 @@ const main = async () => {
|
|
|
25
25
|
throw error; // Rethrow so Bree can handle it properly
|
|
26
26
|
} finally {
|
|
27
27
|
await cacheManager.destroy();
|
|
28
|
+
logger.info(`==========Completed clean-up-expired-cache job==========`);
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
// For Bree compatibility - check if this is running as a worker
|
|
32
33
|
if (require.main === module) {
|
|
33
34
|
// This is called when Bree runs this file as a worker
|
|
34
|
-
|
|
35
|
+
(async () => {
|
|
36
|
+
await main();
|
|
37
|
+
})();
|
|
35
38
|
} else {
|
|
36
39
|
// Export for potential testing or direct usage
|
|
37
40
|
module.exports = main;
|
|
@@ -47,9 +47,11 @@ const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
|
47
47
|
|
|
48
48
|
if (isMainModule) {
|
|
49
49
|
// This is called when Bree runs this file as a worker
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
50
|
+
try {
|
|
51
|
+
await main();
|
|
52
|
+
} catch {
|
|
53
|
+
process.exitCode = 1; // prefer exitCode so stdout/stderr can flush
|
|
54
|
+
}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export default main;
|
|
@@ -37,7 +37,9 @@ const main = async () => {
|
|
|
37
37
|
// For Bree compatibility - check if this is running as a worker
|
|
38
38
|
if (require.main === module) {
|
|
39
39
|
// This is called when Bree runs this file as a worker
|
|
40
|
-
main()
|
|
40
|
+
main().catch(() => {
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
});
|
|
41
43
|
} else {
|
|
42
44
|
// Export for potential testing or direct usage
|
|
43
45
|
module.exports = main;
|
|
@@ -18,4 +18,10 @@ export default async function runMigrations() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
22
|
+
if (isMainModule) {
|
|
23
|
+
runMigrations().catch(err => {
|
|
24
|
+
logger.error("Top-level error running migrations:", err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -113,4 +113,12 @@ export class GraphQLService {
|
|
|
113
113
|
ErrorHandler.handle(error);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cleanup all HTTP connections and resources
|
|
119
|
+
* Call this when the service is no longer needed
|
|
120
|
+
*/
|
|
121
|
+
async destroy(): Promise<void> {
|
|
122
|
+
await this.client.destroy();
|
|
123
|
+
}
|
|
116
124
|
}
|
|
@@ -209,4 +209,12 @@ export class RestAPIService {
|
|
|
209
209
|
ErrorHandler.handle(error);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Cleanup all HTTP connections and resources
|
|
215
|
+
* Call this when the service is no longer needed
|
|
216
|
+
*/
|
|
217
|
+
async destroy(): Promise<void> {
|
|
218
|
+
await this.client.destroy();
|
|
219
|
+
}
|
|
212
220
|
}
|
|
@@ -59,9 +59,8 @@ export class MMApiClient {
|
|
|
59
59
|
[UrlBase.ErpApiSvcBase]: CoreConfiguration.inst().mmERPSvcApiBaseUrl,
|
|
60
60
|
[UrlBase.ApiBase]: CoreConfiguration.inst().mmApiBaseUrl,
|
|
61
61
|
};
|
|
62
|
-
// We'll create HTTP clients as needed for different base URLs
|
|
63
62
|
this.api = HTTPClientFactory.getInstance({
|
|
64
|
-
baseUrl:
|
|
63
|
+
baseUrl: CoreConfiguration.inst().mmERPSvcApiBaseUrl,
|
|
65
64
|
retryAttempts: CoreConfiguration.inst().mmApiRetryAttempts,
|
|
66
65
|
});
|
|
67
66
|
}
|
|
@@ -681,5 +680,15 @@ export class MMApiClient {
|
|
|
681
680
|
)) as unknown as MMGraphQLResourceResponse;
|
|
682
681
|
}
|
|
683
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Cleanup all HTTP connections and resources
|
|
685
|
+
* Call this when the service is no longer needed
|
|
686
|
+
*/
|
|
687
|
+
async destroy(): Promise<void> {
|
|
688
|
+
await this.api.destroy();
|
|
689
|
+
// Note: MMTokenManager doesn't currently need explicit cleanup
|
|
690
|
+
// but if it ever acquires resources that need cleanup, add it here
|
|
691
|
+
}
|
|
692
|
+
|
|
684
693
|
//#endregion public methods
|
|
685
694
|
}
|
|
@@ -69,6 +69,9 @@ export interface MM200LaborTicketResponse extends MMApiBaseResponse {
|
|
|
69
69
|
* Response for partial success of non-labor ticket entities
|
|
70
70
|
* Used for: Resources, Parts, PartOperations, WorkOrders, WorkOrderOperations, Persons, Reasons
|
|
71
71
|
*
|
|
72
|
+
* Note: The API may internally re-batch client submissions. Each error represents
|
|
73
|
+
* a batch that failed due to at least one record having issues.
|
|
74
|
+
*
|
|
72
75
|
* Example response:
|
|
73
76
|
* {
|
|
74
77
|
* message: "WorkOrderOperations partially imported with errors",
|
|
@@ -82,7 +85,7 @@ export interface MM200LaborTicketResponse extends MMApiBaseResponse {
|
|
|
82
85
|
* batch: 1,
|
|
83
86
|
* path: "$.selectionSet.insertErpWorkOrderOperations.args.objects",
|
|
84
87
|
* code: "constraint-violation",
|
|
85
|
-
* batchData: [{ workOrderId: "24-0196", ... }]
|
|
88
|
+
* batchData: [{ workOrderId: "24-0196", ... }] // All records in failing batch
|
|
86
89
|
* }
|
|
87
90
|
* ]
|
|
88
91
|
* },
|
|
@@ -101,7 +104,7 @@ export interface MM207NonLaborTicketResponse extends MMApiBaseResponse {
|
|
|
101
104
|
batch: number;
|
|
102
105
|
path: string;
|
|
103
106
|
code: string;
|
|
104
|
-
batchData: Record<string, unknown>[];
|
|
107
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
105
108
|
}>;
|
|
106
109
|
};
|
|
107
110
|
}
|
|
@@ -109,6 +112,9 @@ export interface MM207NonLaborTicketResponse extends MMApiBaseResponse {
|
|
|
109
112
|
/**
|
|
110
113
|
* Response for partial success of labor tickets
|
|
111
114
|
*
|
|
115
|
+
* Note: The API may internally re-batch client submissions. Each error represents
|
|
116
|
+
* a batch that failed due to at least one record having issues.
|
|
117
|
+
*
|
|
112
118
|
* Example response:
|
|
113
119
|
* {
|
|
114
120
|
* message: "Labor tickets partially imported with errors",
|
|
@@ -121,7 +127,7 @@ export interface MM207NonLaborTicketResponse extends MMApiBaseResponse {
|
|
|
121
127
|
* batch: 1,
|
|
122
128
|
* path: "$.selectionSet.updateErpLaborTickets.args.objects",
|
|
123
129
|
* code: "constraint-violation",
|
|
124
|
-
* batchData: [{ workOrderId: "24-0196", ... }]
|
|
130
|
+
* batchData: [{ workOrderId: "24-0196", ... }] // All records in failing batch
|
|
125
131
|
* }
|
|
126
132
|
* ],
|
|
127
133
|
* insertErrors: [
|
|
@@ -130,7 +136,7 @@ export interface MM207NonLaborTicketResponse extends MMApiBaseResponse {
|
|
|
130
136
|
* batch: 1,
|
|
131
137
|
* path: "$.selectionSet.insertErpLaborTickets.args.objects",
|
|
132
138
|
* code: "constraint-violation",
|
|
133
|
-
* batchData: [{ workOrderId: "24-0191", ... }]
|
|
139
|
+
* batchData: [{ workOrderId: "24-0191", ... }] // All records in failing batch
|
|
134
140
|
* }
|
|
135
141
|
* ]
|
|
136
142
|
* },
|
|
@@ -148,14 +154,14 @@ export interface MM207LaborTicketResponse extends MMApiBaseResponse {
|
|
|
148
154
|
batch: number;
|
|
149
155
|
path: string;
|
|
150
156
|
code: string;
|
|
151
|
-
batchData: Record<string, unknown>[];
|
|
157
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
152
158
|
}>;
|
|
153
159
|
insertErrors: Array<{
|
|
154
160
|
message: string;
|
|
155
161
|
batch: number;
|
|
156
162
|
path: string;
|
|
157
163
|
code: string;
|
|
158
|
-
batchData: Record<string, unknown>[];
|
|
164
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
159
165
|
}>;
|
|
160
166
|
};
|
|
161
167
|
}
|
|
@@ -168,8 +174,13 @@ export interface MM207LaborTicketResponse extends MMApiBaseResponse {
|
|
|
168
174
|
* Exception structure for complete failure of non-labor ticket entities
|
|
169
175
|
* Used for: Resources, Parts, PartOperations, WorkOrders, WorkOrderOperations, Persons, Reasons
|
|
170
176
|
*
|
|
177
|
+
* IMPORTANT: This structure only applies to 500 errors caused by data validation failures.
|
|
178
|
+
* Other 500 errors (network issues, server errors, etc.) may have different structures.
|
|
171
179
|
* This structure appears in caught exceptions when the API returns 500 status
|
|
172
|
-
* for validation issues that contain structured error information.
|
|
180
|
+
* specifically for validation issues that contain structured error information.
|
|
181
|
+
*
|
|
182
|
+
* Note: The API may internally re-batch client submissions. Each error represents
|
|
183
|
+
* a batch that failed due to at least one record having issues.
|
|
173
184
|
*
|
|
174
185
|
* Example exception.data structure:
|
|
175
186
|
* {
|
|
@@ -181,7 +192,7 @@ export interface MM207LaborTicketResponse extends MMApiBaseResponse {
|
|
|
181
192
|
* errors: [
|
|
182
193
|
* {
|
|
183
194
|
* message: "GraphQL Error: Foreign key violation...",
|
|
184
|
-
* batchData: [{ workOrderId: "24-0196", ... }]
|
|
195
|
+
* batchData: [{ workOrderId: "24-0196", ... }] // All records in failing batch
|
|
185
196
|
* }
|
|
186
197
|
* ]
|
|
187
198
|
* }
|
|
@@ -197,7 +208,7 @@ export interface MM500NonLaborTicketException {
|
|
|
197
208
|
affectedRows: number;
|
|
198
209
|
errors: Array<{
|
|
199
210
|
message: string;
|
|
200
|
-
batchData: Record<string, unknown>[];
|
|
211
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
201
212
|
}>;
|
|
202
213
|
};
|
|
203
214
|
};
|
|
@@ -206,8 +217,13 @@ export interface MM500NonLaborTicketException {
|
|
|
206
217
|
/**
|
|
207
218
|
* Exception structure for complete failure of labor tickets
|
|
208
219
|
*
|
|
220
|
+
* IMPORTANT: This structure only applies to 500 errors caused by data validation failures.
|
|
221
|
+
* Other 500 errors (network issues, server errors, etc.) may have different structures.
|
|
209
222
|
* This structure appears in caught exceptions when the API returns 500 status
|
|
210
|
-
* for validation issues that contain structured error information.
|
|
223
|
+
* specifically for validation issues that contain structured error information.
|
|
224
|
+
*
|
|
225
|
+
* Note: The API may internally re-batch client submissions. Each error represents
|
|
226
|
+
* a batch that failed due to at least one record having issues.
|
|
211
227
|
*
|
|
212
228
|
* Example exception.data structure:
|
|
213
229
|
* {
|
|
@@ -216,13 +232,13 @@ export interface MM500NonLaborTicketException {
|
|
|
216
232
|
* updateErrors: [
|
|
217
233
|
* {
|
|
218
234
|
* message: "GraphQL Error: Foreign key violation...",
|
|
219
|
-
* batchData: [{ workOrderId: "24-0196", ... }]
|
|
235
|
+
* batchData: [{ workOrderId: "24-0196", ... }] // All records in failing batch
|
|
220
236
|
* }
|
|
221
237
|
* ],
|
|
222
238
|
* insertErrors: [
|
|
223
239
|
* {
|
|
224
240
|
* message: "GraphQL Error: Foreign key violation...",
|
|
225
|
-
* batchData: [{ workOrderId: "24-0191", ... }]
|
|
241
|
+
* batchData: [{ workOrderId: "24-0191", ... }] // All records in failing batch
|
|
226
242
|
* }
|
|
227
243
|
* ]
|
|
228
244
|
* }
|
|
@@ -235,11 +251,11 @@ export interface MM500LaborTicketException {
|
|
|
235
251
|
message: {
|
|
236
252
|
updateErrors: Array<{
|
|
237
253
|
message: string;
|
|
238
|
-
batchData: Record<string, unknown>[];
|
|
254
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
239
255
|
}>;
|
|
240
256
|
insertErrors: Array<{
|
|
241
257
|
message: string;
|
|
242
|
-
batchData: Record<string, unknown>[];
|
|
258
|
+
batchData: Record<string, unknown>[]; // Complete batch data where at least one record failed
|
|
243
259
|
}>;
|
|
244
260
|
};
|
|
245
261
|
};
|
|
@@ -4,6 +4,7 @@ export const SQLServerConfigSchema = z.object({
|
|
|
4
4
|
password: z.string().nonempty("Password is required."),
|
|
5
5
|
database: z.string().nonempty("Database name is required."),
|
|
6
6
|
server: z.string().nonempty("Server is required."),
|
|
7
|
+
port: z.coerce.number().int().positive("Port must be a positive integer.").default(1433),
|
|
7
8
|
connectionTimeout: z.coerce
|
|
8
9
|
.number()
|
|
9
10
|
.int()
|
package/src/utils/http-client.ts
CHANGED
|
@@ -31,7 +31,8 @@ export interface HTTPResponse<T = unknown> {
|
|
|
31
31
|
|
|
32
32
|
export interface HTTPClient {
|
|
33
33
|
request<T = unknown>(config: HTTPRequestConfig): Promise<HTTPResponse<T>>;
|
|
34
|
-
handleError(error: unknown
|
|
34
|
+
handleError(error: unknown): HTTPError;
|
|
35
|
+
destroy(): Promise<void>;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface HTTPClientConfig {
|
|
@@ -50,8 +51,12 @@ export class HTTPClientFactory {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
class AxiosClient implements HTTPClient {
|
|
53
|
-
private client: AxiosInstance;
|
|
54
|
+
private client: AxiosInstance | null = null;
|
|
54
55
|
private retryAttempts: number;
|
|
56
|
+
private isDestroyed: boolean = false;
|
|
57
|
+
private inFlightControllers: Set<AbortController> = new Set();
|
|
58
|
+
private pendingTimeouts: Set<ReturnType<typeof setTimeout>> = new Set();
|
|
59
|
+
private pendingSleepResolvers: Set<() => void> = new Set();
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
62
|
* Note regarding baseURL, from https://github.com/axios/axios
|
|
@@ -69,15 +74,39 @@ class AxiosClient implements HTTPClient {
|
|
|
69
74
|
this.retryAttempts = retryAttempts;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
private sleep(ms: number): Promise<void> {
|
|
78
|
+
return new Promise<void>((resolve) => {
|
|
79
|
+
if (this.isDestroyed) {
|
|
80
|
+
resolve();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const timeout = setTimeout(() => {
|
|
84
|
+
this.pendingTimeouts.delete(timeout);
|
|
85
|
+
this.pendingSleepResolvers.delete(resolve);
|
|
86
|
+
resolve();
|
|
87
|
+
}, ms);
|
|
88
|
+
this.pendingTimeouts.add(timeout);
|
|
89
|
+
this.pendingSleepResolvers.add(resolve);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
async request<T = unknown>(
|
|
73
94
|
config: HTTPRequestConfig
|
|
74
95
|
): Promise<HTTPResponse<T>> {
|
|
96
|
+
if (this.isDestroyed || !this.client) {
|
|
97
|
+
throw new HTTPError("HTTP client has been destroyed", 500);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
this.inFlightControllers.add(controller);
|
|
102
|
+
|
|
75
103
|
const axiosConfig: AxiosRequestConfig = {
|
|
76
104
|
method: config.method,
|
|
77
105
|
url: config.url,
|
|
78
106
|
headers: config.headers,
|
|
79
107
|
data: config.data,
|
|
80
108
|
params: config.params,
|
|
109
|
+
signal: controller.signal,
|
|
81
110
|
};
|
|
82
111
|
|
|
83
112
|
logger.info("HTTP request starting", {
|
|
@@ -95,47 +124,64 @@ class AxiosClient implements HTTPClient {
|
|
|
95
124
|
console.log("method:", config.method);
|
|
96
125
|
|
|
97
126
|
let lastError: unknown;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
127
|
+
try {
|
|
128
|
+
for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
|
|
129
|
+
try {
|
|
130
|
+
logger.info(`HTTP request attempt ${attempt + 1}/${this.retryAttempts + 1}`);
|
|
131
|
+
const response = await this.client.request<T>(axiosConfig);
|
|
132
|
+
logger.info("HTTP request succeeded", { status: response.status });
|
|
133
|
+
return {
|
|
134
|
+
data: response.data,
|
|
135
|
+
status: response.status,
|
|
136
|
+
headers: response.headers as Record<string, string>,
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
lastError = error;
|
|
140
|
+
|
|
141
|
+
const isAxiosErr = error instanceof AxiosError;
|
|
142
|
+
const code = isAxiosErr ? error.code : undefined;
|
|
143
|
+
const status = isAxiosErr ? error.response?.status : undefined;
|
|
144
|
+
const errorConstructor = error instanceof Error ? error.constructor.name : undefined;
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
146
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
logger.info(`HTTP request attempt ${attempt + 1} failed`, {
|
|
148
|
+
errorType: typeof error,
|
|
149
|
+
errorConstructor,
|
|
150
|
+
isAxiosError: isAxiosErr,
|
|
151
|
+
message,
|
|
152
|
+
code,
|
|
153
|
+
status,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Don't retry on 4xx errors (client errors)
|
|
157
|
+
if (
|
|
158
|
+
error instanceof AxiosError &&
|
|
159
|
+
error.response?.status &&
|
|
160
|
+
error.response.status >= 400 &&
|
|
161
|
+
error.response.status < 500
|
|
162
|
+
) {
|
|
163
|
+
logger.info("Not retrying due to 4xx client error");
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Don't retry canceled/aborted requests
|
|
168
|
+
if (error instanceof AxiosError && error.code === "ERR_CANCELED") {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// If this was the last attempt, don't wait
|
|
173
|
+
if (attempt < this.retryAttempts) {
|
|
174
|
+
const waitTime = Math.pow(2, attempt) * 1000;
|
|
175
|
+
logger.info(`Waiting ${waitTime}ms before retry`);
|
|
176
|
+
await this.sleep(waitTime);
|
|
177
|
+
if (this.isDestroyed) {
|
|
178
|
+
throw new HTTPError("HTTP client has been destroyed", 500);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
137
181
|
}
|
|
138
182
|
}
|
|
183
|
+
} finally {
|
|
184
|
+
this.inFlightControllers.delete(controller);
|
|
139
185
|
}
|
|
140
186
|
logger.info("HTTP request failed after all retries, throwing error");
|
|
141
187
|
throw this.handleError(lastError, config);
|
|
@@ -144,7 +190,7 @@ class AxiosClient implements HTTPClient {
|
|
|
144
190
|
handleError(error: unknown, requestConfig?: HTTPRequestConfig): HTTPError {
|
|
145
191
|
if (error instanceof AxiosError) {
|
|
146
192
|
// Build a more descriptive error message that includes the URL and method
|
|
147
|
-
const baseUrl = this.client
|
|
193
|
+
const baseUrl = this.client?.defaults.baseURL || "";
|
|
148
194
|
const fullUrl = requestConfig
|
|
149
195
|
? `${baseUrl}${requestConfig.url}`
|
|
150
196
|
: "Unknown URL";
|
|
@@ -164,4 +210,28 @@ class AxiosClient implements HTTPClient {
|
|
|
164
210
|
500
|
|
165
211
|
);
|
|
166
212
|
}
|
|
213
|
+
|
|
214
|
+
async destroy(): Promise<void> {
|
|
215
|
+
if (this.isDestroyed) return;
|
|
216
|
+
this.isDestroyed = true;
|
|
217
|
+
|
|
218
|
+
// Abort any in-flight requests
|
|
219
|
+
for (const c of this.inFlightControllers) {
|
|
220
|
+
try { c.abort(); } catch { /* ignore: abort may throw in some environments */ }
|
|
221
|
+
}
|
|
222
|
+
this.inFlightControllers.clear();
|
|
223
|
+
|
|
224
|
+
// Cancel any pending retry waits
|
|
225
|
+
for (const t of this.pendingTimeouts) {
|
|
226
|
+
clearTimeout(t);
|
|
227
|
+
}
|
|
228
|
+
this.pendingTimeouts.clear();
|
|
229
|
+
for (const resolve of this.pendingSleepResolvers) {
|
|
230
|
+
try { resolve(); } catch { /* ignore: resolver threw; cleanup proceeds */ }
|
|
231
|
+
}
|
|
232
|
+
this.pendingSleepResolvers.clear();
|
|
233
|
+
|
|
234
|
+
// Drop axios instance reference
|
|
235
|
+
this.client = null;
|
|
236
|
+
}
|
|
167
237
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -37,6 +37,12 @@ export type {
|
|
|
37
37
|
} from "./standard-process-drivers/";
|
|
38
38
|
export { getCachedTimezoneOffset } from "./local-data-store/jobs-shared-data";
|
|
39
39
|
|
|
40
|
+
// Local data store
|
|
41
|
+
export {
|
|
42
|
+
getInitialLoadComplete,
|
|
43
|
+
setInitialLoadComplete,
|
|
44
|
+
} from "./local-data-store/jobs-shared-data";
|
|
45
|
+
|
|
40
46
|
/**
|
|
41
47
|
* ERP API utilities
|
|
42
48
|
*/
|
|
@@ -22,17 +22,17 @@ export class ErrorProcessor {
|
|
|
22
22
|
entityType: ERPObjType,
|
|
23
23
|
batchErrors: Array<{
|
|
24
24
|
message: string;
|
|
25
|
-
|
|
25
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
26
26
|
}>
|
|
27
27
|
): Set<string> {
|
|
28
28
|
const failedKeySet = new Set<string>();
|
|
29
29
|
|
|
30
30
|
batchErrors.forEach((batchError) => {
|
|
31
|
-
batchError.
|
|
31
|
+
batchError.affectedEntities.forEach((affectedEntity) => {
|
|
32
32
|
try {
|
|
33
33
|
const primaryKey = EntityTransformer.extractPrimaryKey(
|
|
34
34
|
entityType,
|
|
35
|
-
|
|
35
|
+
affectedEntity
|
|
36
36
|
);
|
|
37
37
|
failedKeySet.add(primaryKey);
|
|
38
38
|
} catch (error) {
|
|
@@ -93,7 +93,7 @@ export class ErrorProcessor {
|
|
|
93
93
|
toProcess: IToRESTApiObject[],
|
|
94
94
|
batchErrors: Array<{
|
|
95
95
|
message: string;
|
|
96
|
-
|
|
96
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
97
97
|
}>,
|
|
98
98
|
batchCacheManager: BatchCacheManager
|
|
99
99
|
): Promise<void> {
|
|
@@ -120,7 +120,7 @@ export class ErrorProcessor {
|
|
|
120
120
|
/**
|
|
121
121
|
* Extracts error count and batch errors from MM API response for partial failures (HTTP 207)
|
|
122
122
|
* This supports all entities, including the slightly different format for labor tickets.
|
|
123
|
-
* In case of labor tickets, the updateErrors and insertErrors arrays are combined into
|
|
123
|
+
* In case of labor tickets, the updateErrors and insertErrors arrays are combined into affectedEntities.
|
|
124
124
|
* @param mmApiResponse The full MM API response object
|
|
125
125
|
* @param entityType The type of entity being processed (determines response structure)
|
|
126
126
|
* @returns Object containing errorCount and batchErrors
|
|
@@ -133,7 +133,7 @@ export class ErrorProcessor {
|
|
|
133
133
|
errorCount: number;
|
|
134
134
|
batchErrors: Array<{
|
|
135
135
|
message: string;
|
|
136
|
-
|
|
136
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
137
137
|
}>;
|
|
138
138
|
} {
|
|
139
139
|
// Type the data property with the expected structure for HTTP 207 responses
|
|
@@ -221,13 +221,13 @@ export class ErrorProcessor {
|
|
|
221
221
|
|
|
222
222
|
return {
|
|
223
223
|
message: error.message,
|
|
224
|
-
|
|
224
|
+
affectedEntities: typedErrorEntities,
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
);
|
|
228
228
|
|
|
229
229
|
const errorCount = batchErrors.reduce((total: number, batchError) => {
|
|
230
|
-
return total + batchError.
|
|
230
|
+
return total + batchError.affectedEntities.length;
|
|
231
231
|
}, 0);
|
|
232
232
|
|
|
233
233
|
return {
|
|
@@ -250,7 +250,7 @@ export class ErrorProcessor {
|
|
|
250
250
|
errorCount: number;
|
|
251
251
|
batchErrors: Array<{
|
|
252
252
|
message: string;
|
|
253
|
-
|
|
253
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
254
254
|
}>;
|
|
255
255
|
} | null {
|
|
256
256
|
try {
|
|
@@ -383,12 +383,12 @@ export class ErrorProcessor {
|
|
|
383
383
|
return {
|
|
384
384
|
message:
|
|
385
385
|
typeof err?.message === "string" ? err.message : "Unknown error",
|
|
386
|
-
|
|
386
|
+
affectedEntities: typedErrorEntities,
|
|
387
387
|
};
|
|
388
388
|
});
|
|
389
389
|
|
|
390
390
|
const errorCount = batchErrors.reduce((total: number, batchError) => {
|
|
391
|
-
return total + batchError.
|
|
391
|
+
return total + batchError.affectedEntities.length;
|
|
392
392
|
}, 0);
|
|
393
393
|
|
|
394
394
|
logger.info("writeEntitiesToMM: Extracted 500 error details", {
|
|
@@ -30,7 +30,7 @@ export class MMBatchValidationError extends Error {
|
|
|
30
30
|
public readonly httpStatus: number;
|
|
31
31
|
public readonly batchErrors: Array<{
|
|
32
32
|
message: string;
|
|
33
|
-
|
|
33
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
34
34
|
}>;
|
|
35
35
|
|
|
36
36
|
constructor(options: {
|
|
@@ -42,7 +42,7 @@ export class MMBatchValidationError extends Error {
|
|
|
42
42
|
httpStatus: number;
|
|
43
43
|
batchErrors: Array<{
|
|
44
44
|
message: string;
|
|
45
|
-
|
|
45
|
+
affectedEntities: IToRESTApiObject[]; // All entities in the failing batch
|
|
46
46
|
}>;
|
|
47
47
|
}) {
|
|
48
48
|
super(options.message);
|
|
@@ -113,9 +113,10 @@ export class StandardProcessDrivers {
|
|
|
113
113
|
*
|
|
114
114
|
* } catch (error) {
|
|
115
115
|
* if (error instanceof MMBatchValidationError) {
|
|
116
|
-
* // HTTP 207 - Partial success with some validation errors
|
|
117
|
-
* // HTTP 500 - A specific type
|
|
116
|
+
* // HTTP 207 - Partial success with some batches failing due to validation errors
|
|
117
|
+
* // HTTP 500 - A specific type representing complete failure due to validation issues;
|
|
118
118
|
* // other 500 types represent failure due to other issues and are not converted to MMBatchValidationError
|
|
119
|
+
* // Note: Each batch error contains ALL entities from the failing batch, not necessarily just the failed ones
|
|
119
120
|
*
|
|
120
121
|
* console.log(`⚠️ Batch processing completed with errors (HTTP ${error.httpStatus})`);
|
|
121
122
|
* console.log(`📊 Metrics: ${error.upsertedEntities} upserted, ${error.localDedupeCount} locally deduplicated, ${error.apiDedupeCount} API deduplicated`);
|
|
@@ -124,18 +125,18 @@ export class StandardProcessDrivers {
|
|
|
124
125
|
* // Process specific batch errors for retry or logging
|
|
125
126
|
* error.batchErrors.forEach((batchError, index) => {
|
|
126
127
|
* console.log(`Batch ${index + 1} error: ${batchError.message}`);
|
|
127
|
-
* console.log(`
|
|
128
|
+
* console.log(`All entities in failing batch:`, batchError.affectedEntities);
|
|
128
129
|
*
|
|
129
|
-
* // Example: Queue
|
|
130
|
-
* await queueForRetry(batchError.
|
|
130
|
+
* // Example: Queue entire failing batch for retry (contains both successful and failed entities)
|
|
131
|
+
* await queueForRetry(batchError.affectedEntities);
|
|
131
132
|
* });
|
|
132
133
|
*
|
|
133
134
|
* // Decide whether to continue or halt based on httpStatus
|
|
134
135
|
* if (error.httpStatus === 207) {
|
|
135
|
-
* // Partial success - some
|
|
136
|
+
* // Partial success - some batches processed successfully, others failed
|
|
136
137
|
* console.log('⚠️ Continuing with partial success');
|
|
137
138
|
* } else if (error.httpStatus === 500) {
|
|
138
|
-
* // Complete failure -
|
|
139
|
+
* // Complete failure - all batches failed due to validation issues
|
|
139
140
|
* console.log('🛑 Complete failure - no records processed');
|
|
140
141
|
* throw error; // Re-throw if complete failure should halt the process
|
|
141
142
|
* }
|