@machinemetrics/mm-erp-sdk 0.1.1-beta.6 → 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 +137 -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/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/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
|
@@ -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
|
};
|
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
|
* }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hashed-cache-manager-DkDox9wX.js","sources":["../src/services/data-sync-service/configuration-manager.ts","../src/services/caching-service/hashed-cache-manager.ts"],"sourcesContent":["import \"dotenv/config\";\nimport { configureLogger } from \"../reporting-service/logger\";\nimport { SQLServerConfiguration } from \"../sql-server-erp-service/configuration\";\n\nexport class CoreConfiguration {\n private static instance: CoreConfiguration;\n\n // General Configuration\n public readonly logLevel: string;\n public readonly erpSystem: string;\n public readonly nodeEnv: string;\n\n // MM API (aka \"Mapping\") Service\n public readonly mmERPSvcApiBaseUrl: string;\n public readonly mmApiBaseUrl: string;\n public readonly mmApiAuthToken: string;\n public readonly mmApiRetryAttempts: number;\n\n // Caching (optionally used for interacting with the MM API)\n public readonly cacheTTL: number;\n\n // ERP API Service\n public readonly erpApiRetryAttemptsDef: number; //Retry attempts for ERP API\n public readonly erpApiPagingLimit: number; //Pagination limit for ERP API\n\n // Job timing Intervals\n public readonly fromErpInterval: string;\n public readonly toErpInterval: string;\n public readonly retryLaborTicketsInterval: string;\n public readonly cacheExpirationCheckInterval: string;\n\n private constructor() {\n this.logLevel = process.env.LOG_LEVEL || \"info\";\n this.erpSystem = process.env.ERP_SYSTEM || \"template\";\n this.nodeEnv = process.env.NODE_ENV || \"development\";\n\n //#region MM API (aka \"Mapping\") Service\n /**\n * MM ERP Service REST API URL (typically https://erp-api.svc.machinemetrics.com)\n */\n this.mmERPSvcApiBaseUrl = process.env.MM_MAPPING_SERVICE_URL || \"\";\n\n /**\n * MM REST API URL (typically https://api.machinemetrics.com)\n */\n console.log(\"=== CONFIG DEBUG ===\");\n console.log(\"MM_MAPPING_AUTH_SERVICE_URL env var:\", process.env.MM_MAPPING_AUTH_SERVICE_URL);\n this.mmApiBaseUrl = process.env.MM_MAPPING_AUTH_SERVICE_URL || \"\";\n console.log(\"mmApiBaseUrl set to:\", this.mmApiBaseUrl);\n console.log(\"=== END CONFIG DEBUG ===\");\n\n /**\n * Company Auth Token\n */\n this.mmApiAuthToken = process.env.MM_MAPPING_SERVICE_TOKEN || \"\";\n\n /**\n * Number of retry attempts for MM API calls\n */\n this.mmApiRetryAttempts = parseInt(process.env.RETRY_ATTEMPTS || \"0\");\n //#endregion MM API (aka \"Mapping\") Service\n\n //#region ERP API Service\n /**\n * Number of retry attempts for ERP API calls\n */\n this.erpApiRetryAttemptsDef = parseInt(\n process.env.ERP_API_RETRY_ATTEMPTS_DEF || \"3\"\n );\n\n /**\n * Default pagination limit for ERP API\n */\n this.erpApiPagingLimit = parseInt(process.env.ERP_PAGINATION_LIMIT || \"0\");\n //#endregion ERP API Service\n\n /**\n * For how to define the intervals, see Bree's documentation: https://github.com/breejs/bree\n */\n this.fromErpInterval =\n process.env.FROM_ERP_INTERVAL || process.env.POLL_INTERVAL || \"5 min\";\n this.toErpInterval = process.env.TO_ERP_INTERVAL || \"5 min\";\n this.retryLaborTicketsInterval =\n process.env.RETRY_LABOR_TICKETS_INTERVAL || \"30 min\";\n this.cacheExpirationCheckInterval =\n process.env.CACHE_EXPIRATION_CHECK_INTERVAL || \"5 min\";\n\n /**\n * Cache TTL (in seconds)\n */\n const cacheTTLDef = 7 * 24 * 60 * 60; // 7 days\n this.cacheTTL = parseInt(process.env.CACHE_TTL || cacheTTLDef.toString());\n\n // Configure the logger with our settings\n configureLogger(this.logLevel, this.nodeEnv);\n }\n\n public static inst(): CoreConfiguration {\n if (!CoreConfiguration.instance) {\n CoreConfiguration.instance = new CoreConfiguration();\n }\n return CoreConfiguration.instance;\n }\n}\n\n/**\n * Helper function to get the SQL Server Configuration for collectors that use SQL Server to interact with the ERP\n */\nexport const getSQLServerConfiguration = (): SQLServerConfiguration => {\n return {\n username: process.env.ERP_SQLSERVER_USERNAME || \"\",\n password: process.env.ERP_SQLSERVER_PASSWORD || \"\",\n database: process.env.ERP_SQLSERVER_DATABASE || \"\",\n host:\n process.env.ERP_SQLSERVER_HOST || process.env.ERP_SQLSERVER_SERVER || \"\",\n port: process.env.ERP_SQLSERVER_PORT || \"1433\",\n connectionTimeout: process.env.ERP_SQLSERVER_CONNECTION_TIMEOUT || \"30000\",\n requestTimeout: process.env.ERP_SQLSERVER_REQUEST_TIMEOUT || \"60000\",\n poolMax: process.env.ERP_SQLSERVER_MAX || \"10\",\n poolMin: process.env.ERP_SQLSERVER_MIN || \"0\",\n idleTimeoutMillis:\n process.env.ERP_SQLSERVER_IDLE_TIMEOUT_MMILLIS || \"30000\",\n encrypt: process.env.ERP_SQLSERVER_ENCRYPT === \"true\",\n trustServer: process.env.ERP_SQLSERVER_TRUST_SERVER === \"true\",\n };\n};\n\n/**\n * Parameters required to connect to an ERP system via its API.\n * Contains all the necessary settings to establish a connection and authenticate with an ERP system's API.\n */\nexport class ErpApiConnectionParams {\n constructor(\n public readonly erpApiUrl: string, // Base url of ERP\n public readonly erpApiClientId: string, // Client ID to authenticate with ERP\n public readonly erpApiClientSecret: string, // Client Secret to authenticate with ERP\n public readonly erpApiOrganizationId: string, // Organization / tenant Id\n public readonly erpAuthBaseUrl: string, // Auth base url\n public readonly retryAttempts: number = 3 // Number of retry attempts for API calls\n ) {}\n}\n\n/**\n * Helper function to get the ERP API Connection Parameters\n * Not all connectors use these, but keeping these commonly values in one place may\n * make it easier to set and understand env var names set in App.\n */\nexport const getErpApiConnectionParams = (): ErpApiConnectionParams => {\n return new ErpApiConnectionParams(\n process.env.ERP_API_URL || \"\",\n process.env.ERP_API_CLIENT_ID || \"\",\n process.env.ERP_API_CLIENT_SECRET || \"\",\n process.env.ERP_API_ORGANIZATION_ID || \"\",\n process.env.ERP_AUTH_BASE_URL || \"\",\n parseInt(process.env.ERP_API_RETRY_ATTEMPTS || \"3\")\n );\n};\n","import knex, { Knex } from \"knex\";\nimport config from \"../../knexfile\";\nimport stringify from \"json-stable-stringify\";\nimport XXH from \"xxhashjs\";\nimport { ERPObjType } from \"../../types/erp-types\";\nimport { CacheMetrics } from \"./index\";\nimport { CoreConfiguration } from \"../data-sync-service/configuration-manager\";\nimport { logger } from \"../reporting-service\";\n\ntype HashedCacheManagerOptions = {\n ttl?: number;\n tableName?: string;\n};\n\nexport class HashedCacheManager {\n private static TABLE_NAME = \"sdk_cache\";\n private db: Knex;\n private options: HashedCacheManagerOptions;\n private static readonly SEED = 0xabcd; // Arbitrary seed for hashing\n private isDestroyed: boolean = false;\n private metrics: CacheMetrics = {\n recordCounts: {},\n };\n\n constructor(options?: HashedCacheManagerOptions) {\n this.options = {\n ttl: options?.ttl || CoreConfiguration.inst().cacheTTL,\n tableName: options?.tableName || HashedCacheManager.TABLE_NAME,\n };\n this.db = knex({\n ...config.local,\n pool: {\n min: 0,\n max: 10,\n },\n });\n }\n\n /**\n * Checks if the cache manager is still valid\n * @throws Error if the cache manager has been destroyed\n */\n private checkValid(): void {\n if (this.isDestroyed) {\n throw new Error(\"Cache manager has been destroyed\");\n }\n }\n\n /**\n * Generates a stable hash of a record using JSON stringify + xxhash\n */\n public static hashRecord(record: object): string {\n try {\n const serialized = stringify(record);\n if (!serialized) {\n throw new Error(\"Failed to serialize record for hashing\");\n }\n const hash = XXH.h64(serialized, HashedCacheManager.SEED).toString(16);\n return hash;\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"circular\")) {\n throw new Error(\"Failed to serialize record for hashing\");\n }\n throw error;\n }\n }\n\n /**\n * Gets a record from the cache\n * @param type The type of record\n * @param hash The hash of the record\n * @returns The record if it exists, null otherwise\n */\n private async getRecord(\n type: ERPObjType,\n hash: string\n ): Promise<{ key: string } | null> {\n this.checkValid();\n return this.db(this.options.tableName)\n .select(\"key\")\n .where({ type, key: hash })\n .first();\n }\n\n /**\n * Stores a record in the cache\n * @param type The type of record\n * @param record The record to store\n * @returns true if a new record was created, false if an existing record was updated\n */\n public async store(type: ERPObjType, record: object): Promise<boolean> {\n if (!this.isDestroyed && record) {\n try {\n const hash = HashedCacheManager.hashRecord(record);\n const now = new Date();\n\n // First check if record exists with same type and hash\n const existing = await this.db(this.options.tableName)\n .where({\n type,\n key: hash,\n })\n .first();\n\n if (existing) {\n return false; // No need to update, hash hasn't changed\n } else {\n // Insert new record with minimal data\n const result = await this.db(this.options.tableName)\n .insert({\n type,\n key: hash,\n created_at: now,\n })\n .returning(\"id\");\n return result.length > 0;\n }\n } catch (error) {\n logger.error(\"Error storing record:\", error);\n throw error;\n }\n }\n return false;\n }\n\n /**\n * Checks if a record has changed since last seen\n * @param type The type of record\n * @param record The record to check\n * @returns true if the record has changed or is new\n */\n async hasChanged(type: ERPObjType, record: object): Promise<boolean> {\n this.checkValid();\n const newHash = HashedCacheManager.hashRecord(record);\n const existing = await this.getRecord(type, newHash);\n return !existing;\n }\n\n /**\n * Checks if a record has changed and stores it if it has\n * @param type The type of record\n * @param record The record to check and store\n * @returns true if the record was changed or is new\n */\n async upsert(type: ERPObjType, record: object): Promise<boolean> {\n this.checkValid();\n const hasChanged = await this.hasChanged(type, record);\n if (hasChanged) {\n await this.store(type, record as Record<string, unknown>);\n }\n return hasChanged;\n }\n\n /**\n * Removes expired records based on TTL\n */\n async removeExpiredObjects(): Promise<void> {\n this.checkValid();\n const ttl = this.options.ttl;\n if (!ttl) return;\n\n const ttlMilliseconds = ttl * 1000;\n const expirationLimitDate = new Date(Date.now() - ttlMilliseconds);\n const expirationLimit = expirationLimitDate\n .toISOString()\n .slice(0, 19)\n .replace(\"T\", \" \");\n\n await this.db(this.options.tableName)\n .where(\"created_at\", \"<\", expirationLimit)\n .del();\n }\n\n /**\n * Gets all records of a specific type\n */\n async getRecordsByType(type: ERPObjType): Promise<string[]> {\n this.checkValid();\n const records = await this.db(this.options.tableName)\n .select(\"key\")\n .where({ type });\n\n return records.map((record) => record.key);\n }\n\n /**\n * Removes all records of a specific type\n */\n async removeRecordsByType(type: ERPObjType): Promise<void> {\n this.checkValid();\n await this.db(this.options.tableName).where({ type }).del();\n }\n\n /**\n * Removes a specific record\n */\n public async removeRecord(type: ERPObjType, record: object): Promise<void> {\n if (!this.isDestroyed) {\n try {\n const hash = HashedCacheManager.hashRecord(record);\n await this.db(this.options.tableName)\n .where({ type, key: hash }) // Use key for deletion\n .del();\n } catch (error) {\n logger.error(\"Error removing record:\", error);\n throw error;\n }\n }\n }\n\n /**\n * Clears all records from the cache\n */\n async clear(): Promise<void> {\n this.checkValid();\n await this.db(this.options.tableName).del();\n }\n\n /**\n * Cleans up database connection and marks the cache manager as destroyed\n */\n async destroy(): Promise<void> {\n if (!this.isDestroyed) {\n await this.db.destroy();\n this.isDestroyed = true;\n }\n }\n\n /**\n * Gets the current cache metrics\n * @returns The current cache metrics\n */\n async getMetrics(): Promise<CacheMetrics> {\n this.checkValid();\n\n // Get counts for each type\n const counts = (await this.db(this.options.tableName)\n .select(\"type\")\n .count(\"* as count\")\n .groupBy(\"type\")) as Array<{ type: string; count: string }>;\n\n // Update metrics\n this.metrics.recordCounts = counts.reduce(\n (acc, row) => {\n acc[row.type] = parseInt(row.count, 10);\n return acc;\n },\n {} as Record<string, number>\n );\n\n return this.metrics;\n }\n}\n"],"names":[],"mappings":"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"}
|