@machinemetrics/mm-erp-sdk 0.1.1-beta.6 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{hashed-cache-manager-DkDox9wX.js → hashed-cache-manager-Ci59eC75.js} +2 -7
  2. package/dist/hashed-cache-manager-Ci59eC75.js.map +1 -0
  3. package/dist/{index-Cn9ccxOO.js → index-CXbOvFyf.js} +3 -1
  4. package/dist/{index-Cn9ccxOO.js.map → index-CXbOvFyf.js.map} +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/mm-erp-sdk.js +136 -51
  8. package/dist/mm-erp-sdk.js.map +1 -1
  9. package/dist/services/data-sync-service/configuration-manager.d.ts +0 -1
  10. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
  11. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +6 -3
  12. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
  13. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  14. package/dist/services/data-sync-service/jobs/from-erp.js +6 -4
  15. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  16. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -1
  17. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
  18. package/dist/services/data-sync-service/jobs/run-migrations.js +7 -1
  19. package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
  20. package/dist/services/erp-api-services/graphql/graphql-service.d.ts +5 -0
  21. package/dist/services/erp-api-services/graphql/graphql-service.d.ts.map +1 -1
  22. package/dist/services/erp-api-services/rest/rest-api-service.d.ts +5 -0
  23. package/dist/services/erp-api-services/rest/rest-api-service.d.ts.map +1 -1
  24. package/dist/services/mm-api-service/mm-api-service.d.ts +5 -0
  25. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  26. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts +24 -8
  27. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts.map +1 -1
  28. package/dist/utils/http-client.d.ts +2 -1
  29. package/dist/utils/http-client.d.ts.map +1 -1
  30. package/dist/utils/index.d.ts +1 -0
  31. package/dist/utils/index.d.ts.map +1 -1
  32. package/dist/utils/standard-process-drivers/error-processor.d.ts +5 -5
  33. package/dist/utils/standard-process-drivers/error-processor.d.ts.map +1 -1
  34. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts +10 -9
  35. package/dist/utils/standard-process-drivers/standard-process-drivers.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/index.ts +6 -2
  38. package/src/services/data-sync-service/configuration-manager.ts +0 -9
  39. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +4 -1
  40. package/src/services/data-sync-service/jobs/from-erp.ts +5 -3
  41. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +3 -1
  42. package/src/services/data-sync-service/jobs/run-migrations.ts +7 -1
  43. package/src/services/erp-api-services/graphql/graphql-service.ts +8 -0
  44. package/src/services/erp-api-services/rest/rest-api-service.ts +8 -0
  45. package/src/services/mm-api-service/mm-api-service.ts +11 -2
  46. package/src/services/mm-api-service/types/mm-response-interfaces.ts +30 -14
  47. package/src/utils/http-client.ts +111 -41
  48. package/src/utils/index.ts +6 -0
  49. package/src/utils/standard-process-drivers/error-processor.ts +11 -11
  50. package/src/utils/standard-process-drivers/standard-process-drivers.ts +10 -9
  51. 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
- runMigrations();
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: "", // We'll set the full URL in each request
63
+ baseUrl: '',
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
  };
@@ -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, requestConfig?: HTTPRequestConfig): HTTPError;
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
- for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {
99
- try {
100
- logger.info(`HTTP request attempt ${attempt + 1}/${this.retryAttempts + 1}`);
101
- const response = await this.client.request<T>(axiosConfig);
102
- logger.info("HTTP request succeeded", { status: response.status });
103
- return {
104
- data: response.data,
105
- status: response.status,
106
- headers: response.headers as Record<string, string>,
107
- };
108
- } catch (error) {
109
- lastError = error;
110
- logger.info(`HTTP request attempt ${attempt + 1} failed`, {
111
- errorType: typeof error,
112
- errorConstructor: error?.constructor?.name,
113
- isAxiosError: error instanceof AxiosError,
114
- message: error instanceof Error ? error.message : String(error),
115
- code: (error as any)?.code,
116
- status: (error as any)?.response?.status
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
- // Don't retry on 4xx errors (client errors)
120
- if (
121
- error instanceof AxiosError &&
122
- error.response?.status &&
123
- error.response.status >= 400 &&
124
- error.response.status < 500
125
- ) {
126
- logger.info("Not retrying due to 4xx client error");
127
- break;
128
- }
129
- // If this was the last attempt, don't wait
130
- if (attempt < this.retryAttempts) {
131
- const waitTime = Math.pow(2, attempt) * 1000;
132
- logger.info(`Waiting ${waitTime}ms before retry`);
133
- // Exponential backoff: wait 2^attempt * 1000ms
134
- await new Promise((resolve) =>
135
- setTimeout(resolve, Math.pow(2, attempt) * 1000)
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.defaults.baseURL || "";
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
  }
@@ -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
- errorEntities: IToRESTApiObject[];
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.errorEntities.forEach((errorEntity) => {
31
+ batchError.affectedEntities.forEach((affectedEntity) => {
32
32
  try {
33
33
  const primaryKey = EntityTransformer.extractPrimaryKey(
34
34
  entityType,
35
- errorEntity
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
- errorEntities: IToRESTApiObject[];
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 errorEntities.
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
- errorEntities: IToRESTApiObject[];
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
- errorEntities: typedErrorEntities,
224
+ affectedEntities: typedErrorEntities,
225
225
  };
226
226
  }
227
227
  );
228
228
 
229
229
  const errorCount = batchErrors.reduce((total: number, batchError) => {
230
- return total + batchError.errorEntities.length;
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
- errorEntities: IToRESTApiObject[];
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
- errorEntities: typedErrorEntities,
386
+ affectedEntities: typedErrorEntities,
387
387
  };
388
388
  });
389
389
 
390
390
  const errorCount = batchErrors.reduce((total: number, batchError) => {
391
- return total + batchError.errorEntities.length;
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
- errorEntities: IToRESTApiObject[];
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
- errorEntities: IToRESTApiObject[];
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 of which represent complete failure due to validation issues;
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(`Affected entities:`, batchError.errorEntities);
128
+ * console.log(`All entities in failing batch:`, batchError.affectedEntities);
128
129
  *
129
- * // Example: Queue failed entities for retry
130
- * await queueForRetry(batchError.errorEntities);
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 records processed successfully
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 - no records processed
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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"}