@machinemetrics/mm-erp-sdk 0.1.3 → 0.1.4-beta.0

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 (45) hide show
  1. package/dist/{config-2l5vnNkA.js → config-WKwu1mMo.js} +6 -6
  2. package/dist/{config-2l5vnNkA.js.map → config-WKwu1mMo.js.map} +1 -1
  3. package/dist/{connector-factory-CQ8e7Tae.js → connector-factory-DFv3ex0X.js} +2 -2
  4. package/dist/{connector-factory-CQ8e7Tae.js.map → connector-factory-DFv3ex0X.js.map} +1 -1
  5. package/dist/{hashed-cache-manager-Ci59eC75.js → hashed-cache-manager-INiCs0JC.js} +4 -4
  6. package/dist/{hashed-cache-manager-Ci59eC75.js.map → hashed-cache-manager-INiCs0JC.js.map} +1 -1
  7. package/dist/{index-CXbOvFyf.js → index-aci_wdcn.js} +7 -7
  8. package/dist/{index-CXbOvFyf.js.map → index-aci_wdcn.js.map} +1 -1
  9. package/dist/index.d.ts +6 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/{logger-QG73MndU.js → logger-hqtl8hFM.js} +6 -6
  12. package/dist/{logger-QG73MndU.js.map → logger-hqtl8hFM.js.map} +1 -1
  13. package/dist/mm-erp-sdk.js +389 -7
  14. package/dist/mm-erp-sdk.js.map +1 -1
  15. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +4 -4
  16. package/dist/services/data-sync-service/jobs/from-erp.js +4 -4
  17. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -3
  18. package/dist/services/data-sync-service/jobs/run-migrations.js +1 -1
  19. package/dist/services/data-sync-service/jobs/to-erp.js +3 -3
  20. package/dist/services/erp-api-services/index.d.ts +4 -1
  21. package/dist/services/erp-api-services/index.d.ts.map +1 -1
  22. package/dist/services/mm-api-service/mm-api-service.d.ts +20 -0
  23. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  24. package/dist/utils/connector-log/log-deduper.d.ts +56 -0
  25. package/dist/utils/connector-log/log-deduper.d.ts.map +1 -0
  26. package/dist/utils/connector-log/mm-connector-logger-example.d.ts +1 -0
  27. package/dist/utils/connector-log/mm-connector-logger-example.d.ts.map +1 -0
  28. package/dist/utils/connector-log/mm-connector-logger.d.ts +74 -0
  29. package/dist/utils/connector-log/mm-connector-logger.d.ts.map +1 -0
  30. package/dist/utils/error-utils.d.ts +2 -0
  31. package/dist/utils/error-utils.d.ts.map +1 -0
  32. package/dist/utils/index.d.ts +9 -1
  33. package/dist/utils/index.d.ts.map +1 -1
  34. package/dist/utils/standard-process-drivers/index.d.ts +2 -1
  35. package/dist/utils/standard-process-drivers/index.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/index.ts +8 -2
  38. package/src/services/erp-api-services/index.ts +6 -1
  39. package/src/services/mm-api-service/mm-api-service.ts +28 -0
  40. package/src/utils/connector-log/log-deduper.ts +282 -0
  41. package/src/utils/connector-log/mm-connector-logger-example.ts +97 -0
  42. package/src/utils/connector-log/mm-connector-logger.ts +177 -0
  43. package/src/utils/error-utils.ts +18 -0
  44. package/src/utils/index.ts +10 -4
  45. package/src/utils/standard-process-drivers/index.ts +2 -4
@@ -1,15 +1,16 @@
1
- import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-Ci59eC75.js";
2
- import { g, a } from "./hashed-cache-manager-Ci59eC75.js";
3
- import { l as logger } from "./logger-QG73MndU.js";
4
- import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-CXbOvFyf.js";
5
- import { c, d } from "./index-CXbOvFyf.js";
1
+ import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-INiCs0JC.js";
2
+ import { g, a } from "./hashed-cache-manager-INiCs0JC.js";
3
+ import { l as logger } from "./logger-hqtl8hFM.js";
4
+ import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-aci_wdcn.js";
5
+ import { c, d } from "./index-aci_wdcn.js";
6
6
  import axios, { AxiosError } from "axios";
7
7
  import knex from "knex";
8
8
  import { c as config } from "./knexfile-1qKKIORB.js";
9
- import "./connector-factory-CQ8e7Tae.js";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import "./connector-factory-DFv3ex0X.js";
10
12
  import Bree from "bree";
11
13
  import Graceful from "@ladjs/graceful";
12
- import path from "path";
13
14
  import { fileURLToPath } from "url";
14
15
  import sql from "mssql";
15
16
  import { z } from "zod";
@@ -928,6 +929,38 @@ class MMApiClient {
928
929
  (ticket) => new MMReceiveLaborTicket(ticket)
929
930
  );
930
931
  }
932
+ /**
933
+ * Send connector logs to the MM API
934
+ * @param logEntry Single log entry to send
935
+ * @returns Promise with the API response
936
+ */
937
+ async sendConnectorLog(logEntry) {
938
+ return await this.postData(
939
+ "/connector/logs",
940
+ logEntry,
941
+ {},
942
+ {
943
+ baseUrl: "ApiBase"
944
+ /* ApiBase */
945
+ }
946
+ );
947
+ }
948
+ /**
949
+ * Send bulk connector logs to the MM API
950
+ * @param logs Array of log entries to send
951
+ * @returns Promise with the API response
952
+ */
953
+ async sendBulkConnectorLogs(logs) {
954
+ return await this.postData(
955
+ "/connector/logs",
956
+ { logs },
957
+ {},
958
+ {
959
+ baseUrl: "ApiBase"
960
+ /* ApiBase */
961
+ }
962
+ );
963
+ }
931
964
  async deleteFailedLaborTicketIds(system, laborTicketRefs) {
932
965
  return await this.postData(
933
966
  `${this.resourceURLs[ERPObjType.LABOR_TICKETS]}/failed/remove`,
@@ -3041,6 +3074,351 @@ function getERPAPITypeFromEntity(entity, entityMap) {
3041
3074
  );
3042
3075
  return entry ? Number(entry[0]) : void 0;
3043
3076
  }
3077
+ const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
3078
+ function getErrorType(error) {
3079
+ if (error && typeof error === "object") {
3080
+ const o = error;
3081
+ if (isNonEmptyString(o.code)) return o.code;
3082
+ if (isNonEmptyString(o.name)) return o.name;
3083
+ const ctorName = o.constructor?.name;
3084
+ if (isNonEmptyString(ctorName) && ctorName !== "Object") return ctorName;
3085
+ }
3086
+ return "Error";
3087
+ }
3088
+ class LogEntry {
3089
+ level;
3090
+ message;
3091
+ dedupeKey;
3092
+ eventTime;
3093
+ constructor(params) {
3094
+ this.level = params.level;
3095
+ this.message = params.message;
3096
+ this.dedupeKey = params.dedupeKey;
3097
+ this.eventTime = Date.now();
3098
+ }
3099
+ }
3100
+ function isLogResponse(value) {
3101
+ if (value === null || typeof value !== "object") return false;
3102
+ const v = value;
3103
+ if (typeof v.message !== "string") return false;
3104
+ if ("processed" in v && typeof v.processed !== "number") return false;
3105
+ return true;
3106
+ }
3107
+ class MMConnectorLogger {
3108
+ MAX_MSG_LEN = 2e3;
3109
+ mmApiClient;
3110
+ deduper;
3111
+ source;
3112
+ constructor(source, deduper) {
3113
+ if (source.length < 1 || source.length > 64) {
3114
+ throw new Error("source must be 1-64 characters");
3115
+ }
3116
+ this.mmApiClient = new MMApiClient();
3117
+ this.deduper = deduper;
3118
+ this.source = source;
3119
+ }
3120
+ // Deduplication helpers are delegated to injected FileLogDeduper
3121
+ /**
3122
+ * Send a single log entry to the MM cloud with deduplication.
3123
+ *
3124
+ * The deduplication is handled by the injected LogDeduper.
3125
+ * If no deduper is injected, the log entry is sent without deduplication.
3126
+ *
3127
+ * The standard deduper, FileLogDeduper, stores the deduplication state in a file,
3128
+ * allowing deduplication across jobs,
3129
+ *
3130
+ * @param logEntry - The log entry to send
3131
+ * @returns Promise resolving to the API response or null if suppressed
3132
+ * @throws HTTPError if the request fails or Error if the log entry is invalid
3133
+ */
3134
+ async sendLog(logEntry) {
3135
+ this.validateLogEntry(logEntry);
3136
+ const now = Date.now();
3137
+ let messageToSend = logEntry.message;
3138
+ if (this.deduper) {
3139
+ const decision = await this.deduper.decide(logEntry, now);
3140
+ if (decision === null) return null;
3141
+ messageToSend = decision;
3142
+ }
3143
+ try {
3144
+ const logEntryToSend = {
3145
+ source: this.source,
3146
+ level: logEntry.level,
3147
+ message: messageToSend
3148
+ };
3149
+ const response = await this.mmApiClient.sendConnectorLog(logEntryToSend);
3150
+ if (this.deduper) {
3151
+ await this.deduper.onSuccess(logEntry, now);
3152
+ }
3153
+ if (!isLogResponse(response)) {
3154
+ logger.warn("Unexpected success response format from MM API for connector log", { response });
3155
+ return { message: "Unexpected success response format when sending log" };
3156
+ }
3157
+ return { message: response.message };
3158
+ } catch (error) {
3159
+ logger.error("Failed to send log to MM cloud", {
3160
+ level: logEntry.level,
3161
+ error: error instanceof Error ? error.message : "Unknown error"
3162
+ });
3163
+ throw error;
3164
+ }
3165
+ }
3166
+ /**
3167
+ * @throws Error if validation fails
3168
+ */
3169
+ validateLogEntry(logEntry) {
3170
+ const allowedLevels = ["info", "warn", "error"];
3171
+ if (!logEntry.level || !allowedLevels.includes(logEntry.level)) {
3172
+ throw new Error(`level must be one of: ${allowedLevels.join(", ")}`);
3173
+ }
3174
+ if (!logEntry.message || typeof logEntry.message !== "string") {
3175
+ throw new Error("message is required and must be a string");
3176
+ }
3177
+ logEntry.message = logEntry.message.slice(0, this.MAX_MSG_LEN);
3178
+ if (!logEntry.dedupeKey || typeof logEntry.dedupeKey !== "string") {
3179
+ throw new Error("dedupeKey is required and must be a string");
3180
+ }
3181
+ if (logEntry.dedupeKey.trim().length < 1) {
3182
+ throw new Error("dedupeKey must be a non-empty string");
3183
+ }
3184
+ }
3185
+ /**
3186
+ * Retry all failed transmissions silently
3187
+ * This method attempts to retry all messages that failed to transmit
3188
+ * and removes them from the failed list if successful, else leaves them for the client to retry
3189
+ *
3190
+ * Expected usage is by a client to call this as part of its own retry mechanism
3191
+ */
3192
+ async retryFailedTransmissions() {
3193
+ if (!this.deduper || !this.deduper.retryFailedTransmissions) {
3194
+ return;
3195
+ }
3196
+ await this.deduper.retryFailedTransmissions(async (entry, message) => {
3197
+ await this.mmApiClient.sendConnectorLog({
3198
+ source: this.source,
3199
+ level: entry.level,
3200
+ message
3201
+ });
3202
+ });
3203
+ }
3204
+ /**
3205
+ * Clean up resources
3206
+ */
3207
+ async destroy() {
3208
+ await this.mmApiClient.destroy();
3209
+ }
3210
+ }
3211
+ class FileLogDeduper {
3212
+ storeFilePath;
3213
+ windowMs;
3214
+ ttlMs;
3215
+ sweepIntervalMs;
3216
+ lastSweepTsMs;
3217
+ DEFAULT_WINDOW_TEN_MINS = 600;
3218
+ DEFAULT_TTL_ONE_HOUR = 3600;
3219
+ DEFAULT_SWEEP_INTERVAL_FIVE_MINS = 300;
3220
+ DEFAULT_STORE_FILE_PATH = path.join("/tmp", "log-deduplication.json");
3221
+ /**
3222
+ * Ctor.
3223
+ * @param storeFilePath: The path to the file where the deduplication store is stored; recommended is to use the default
3224
+ * @param windowSeconds: Suppression window. Duplicates within this period are suppressed.
3225
+ * @param ttlSeconds: Eviction TTL. Store entries for keys inactive beyond this are removed. Enforced to be ≥ windowSeconds.
3226
+ * @param sweepIntervalSeconds: Efficiency parameter. How often (min interval) to run opportunistic eviction; retry always sweeps
3227
+ * The sweep is lazy, used only when the store is accessed
3228
+ */
3229
+ constructor({
3230
+ storeFilePath = this.DEFAULT_STORE_FILE_PATH,
3231
+ windowSeconds = this.DEFAULT_WINDOW_TEN_MINS,
3232
+ ttlSeconds = this.DEFAULT_TTL_ONE_HOUR,
3233
+ sweepIntervalSeconds = this.DEFAULT_SWEEP_INTERVAL_FIVE_MINS
3234
+ } = {}) {
3235
+ this.storeFilePath = storeFilePath;
3236
+ this.windowMs = Math.max(1, windowSeconds) * 1e3;
3237
+ this.ttlMs = Math.max(this.windowMs, Math.max(1, ttlSeconds) * 1e3);
3238
+ this.sweepIntervalMs = Math.max(1, sweepIntervalSeconds) * 1e3;
3239
+ this.lastSweepTsMs = 0;
3240
+ this.ensureStoreFileExists();
3241
+ }
3242
+ /**
3243
+ * Deduplication gating function
3244
+ * Returns the formatted message to send, or null to suppress
3245
+ * Decision is based on the dedupeKey and the time of the entry
3246
+ */
3247
+ async decide(entry, now) {
3248
+ if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
3249
+ throw new Error("dedupeKey is required and must be a non-empty string");
3250
+ }
3251
+ const key = entry.dedupeKey;
3252
+ return this.withLock(async () => {
3253
+ const store = this.readStore();
3254
+ if (now - this.lastSweepTsMs >= this.sweepIntervalMs) {
3255
+ this.evictExpiredInStore(store, now);
3256
+ this.lastSweepTsMs = now;
3257
+ this.writeStore(store);
3258
+ }
3259
+ const existing = store[key];
3260
+ if (existing) {
3261
+ const withinWindow = existing.lastTransmitted > 0 && existing.lastTransmitted + this.windowMs > now;
3262
+ if (withinWindow) {
3263
+ store[key] = {
3264
+ ...existing,
3265
+ suppressedCount: existing.suppressedCount + 1,
3266
+ lastEventTs: entry.eventTime ?? now,
3267
+ level: entry.level,
3268
+ message: entry.message
3269
+ };
3270
+ this.writeStore(store);
3271
+ return null;
3272
+ }
3273
+ const messageToSend2 = this.formatMessage(entry.message, entry.eventTime ?? now, existing.suppressedCount);
3274
+ store[key] = {
3275
+ ...existing,
3276
+ suppressedCount: 0,
3277
+ lastEventTs: entry.eventTime ?? now,
3278
+ level: entry.level,
3279
+ message: entry.message
3280
+ };
3281
+ this.writeStore(store);
3282
+ return messageToSend2;
3283
+ }
3284
+ const messageToSend = this.formatMessage(entry.message, entry.eventTime ?? now, 0);
3285
+ store[key] = {
3286
+ lastTransmitted: 0,
3287
+ suppressedCount: 0,
3288
+ firstUnsentEventTs: entry.eventTime ?? now,
3289
+ lastEventTs: entry.eventTime ?? now,
3290
+ level: entry.level,
3291
+ message: entry.message
3292
+ };
3293
+ this.writeStore(store);
3294
+ return messageToSend;
3295
+ });
3296
+ }
3297
+ async onSuccess(entry, now) {
3298
+ if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
3299
+ throw new Error("dedupeKey is required and must be a non-empty string");
3300
+ }
3301
+ const key = entry.dedupeKey;
3302
+ await this.withLock(async () => {
3303
+ const store = this.readStore();
3304
+ const existing = store[key];
3305
+ if (existing) {
3306
+ store[key] = {
3307
+ ...existing,
3308
+ lastTransmitted: now,
3309
+ firstUnsentEventTs: 0,
3310
+ suppressedCount: 0
3311
+ };
3312
+ this.writeStore(store);
3313
+ }
3314
+ });
3315
+ }
3316
+ async retryFailedTransmissions(send) {
3317
+ const now = Date.now();
3318
+ const entries = await this.withLock(async () => {
3319
+ const store = this.readStore();
3320
+ this.evictExpiredInStore(store, now);
3321
+ this.lastSweepTsMs = now;
3322
+ this.writeStore(store);
3323
+ return Object.entries(store).filter(([, rec]) => rec.lastTransmitted === 0).map(([key, rec]) => ({ key, rec }));
3324
+ });
3325
+ for (const { key, rec } of entries) {
3326
+ try {
3327
+ const message = this.formatMessage(rec.message, rec.lastEventTs, rec.suppressedCount, rec.firstUnsentEventTs);
3328
+ await send({ level: rec.level, message: rec.message, dedupeKey: key, eventTime: rec.lastEventTs }, message);
3329
+ await this.withLock(async () => {
3330
+ const store = this.readStore();
3331
+ const current = store[key];
3332
+ if (current) {
3333
+ store[key] = {
3334
+ ...current,
3335
+ lastTransmitted: Date.now(),
3336
+ suppressedCount: 0
3337
+ };
3338
+ this.writeStore(store);
3339
+ }
3340
+ });
3341
+ } catch (err) {
3342
+ logger.error("Failed to retry failed transmission", { key, rec, error: err });
3343
+ return;
3344
+ }
3345
+ }
3346
+ }
3347
+ // --- Internals ---
3348
+ ensureStoreFileExists() {
3349
+ try {
3350
+ if (!fs.existsSync(this.storeFilePath)) {
3351
+ fs.writeFileSync(this.storeFilePath, JSON.stringify({}), "utf-8");
3352
+ }
3353
+ } catch {
3354
+ }
3355
+ }
3356
+ readStore() {
3357
+ try {
3358
+ if (!fs.existsSync(this.storeFilePath)) return {};
3359
+ const content = fs.readFileSync(this.storeFilePath, "utf-8");
3360
+ return content ? JSON.parse(content) : {};
3361
+ } catch {
3362
+ return {};
3363
+ }
3364
+ }
3365
+ writeStore(store) {
3366
+ try {
3367
+ fs.writeFileSync(this.storeFilePath, JSON.stringify(store, null, 2), "utf-8");
3368
+ } catch {
3369
+ }
3370
+ }
3371
+ formatMessage(message, eventTs, suppressedCount, firstUnsentEventTs) {
3372
+ const timestamp = new Date(eventTs).toISOString();
3373
+ const base = `${timestamp} | ${message}`;
3374
+ if (suppressedCount > 0) {
3375
+ const since = firstUnsentEventTs && firstUnsentEventTs > 0 ? ` since ${new Date(firstUnsentEventTs).toISOString()}` : "";
3376
+ return `${base} (${suppressedCount} suppressed${since})`;
3377
+ }
3378
+ return base;
3379
+ }
3380
+ async withLock(fn) {
3381
+ const lockPath = `${this.storeFilePath}.lock`;
3382
+ const start = Date.now();
3383
+ while (true) {
3384
+ try {
3385
+ const fd = fs.openSync(lockPath, "wx");
3386
+ try {
3387
+ const result = await fn();
3388
+ return result;
3389
+ } finally {
3390
+ try {
3391
+ fs.closeSync(fd);
3392
+ } catch {
3393
+ }
3394
+ try {
3395
+ fs.unlinkSync(lockPath);
3396
+ } catch {
3397
+ }
3398
+ }
3399
+ } catch {
3400
+ if (Date.now() - start > 3e3) {
3401
+ return await fn();
3402
+ }
3403
+ await new Promise((resolve) => setTimeout(resolve, 50));
3404
+ }
3405
+ }
3406
+ }
3407
+ /**
3408
+ * Evict expired entries from the store based on the TTL and the key's last transmitted time
3409
+ */
3410
+ evictExpiredInStore(store, now) {
3411
+ const keys = Object.keys(store);
3412
+ if (keys.length === 0) return;
3413
+ for (const key of keys) {
3414
+ const rec = store[key];
3415
+ const referenceTs = rec.lastTransmitted > 0 ? rec.lastTransmitted : rec.lastEventTs;
3416
+ if (now - referenceTs > this.ttlMs) {
3417
+ delete store[key];
3418
+ }
3419
+ }
3420
+ }
3421
+ }
3044
3422
  class ApplicationInitializer {
3045
3423
  /**
3046
3424
  * Performs all necessary application initialization tasks
@@ -3592,9 +3970,12 @@ export {
3592
3970
  ERPObjType,
3593
3971
  ERPType,
3594
3972
  ErrorHandler,
3973
+ FileLogDeduper,
3595
3974
  GraphQLService,
3596
3975
  HTTPClientFactory,
3597
3976
  MMApiClient,
3977
+ MMBatchValidationError,
3978
+ MMConnectorLogger,
3598
3979
  MMSendLaborTicket,
3599
3980
  MMSendPart,
3600
3981
  MMSendPartOperation,
@@ -3617,6 +3998,7 @@ export {
3617
3998
  formatDateWithTZOffset,
3618
3999
  getCachedTimezoneOffset,
3619
4000
  g as getErpApiConnectionParams,
4001
+ getErrorType,
3620
4002
  c as getInitialLoadComplete,
3621
4003
  getPayloadWithoutIDField,
3622
4004
  a as getSQLServerConfiguration,