@machinemetrics/mm-erp-sdk 0.9.3 → 0.9.4

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 (53) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/services/data-sync-service/configuration-manager.d.ts.map +1 -1
  5. package/dist/services/data-sync-service/configuration-manager.js +14 -7
  6. package/dist/services/data-sync-service/configuration-manager.js.map +1 -1
  7. package/dist/services/data-sync-service/nats-labor-ticket-listener.d.ts.map +1 -1
  8. package/dist/services/data-sync-service/nats-labor-ticket-listener.js +20 -1
  9. package/dist/services/data-sync-service/nats-labor-ticket-listener.js.map +1 -1
  10. package/dist/services/erp-api-services/errors.d.ts.map +1 -1
  11. package/dist/services/erp-api-services/errors.js +3 -0
  12. package/dist/services/erp-api-services/errors.js.map +1 -1
  13. package/dist/services/mm-api-service/graphql-string-literal.d.ts +10 -0
  14. package/dist/services/mm-api-service/graphql-string-literal.d.ts.map +1 -0
  15. package/dist/services/mm-api-service/graphql-string-literal.js +12 -0
  16. package/dist/services/mm-api-service/graphql-string-literal.js.map +1 -0
  17. package/dist/services/mm-api-service/index.d.ts +2 -2
  18. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  19. package/dist/services/mm-api-service/index.js.map +1 -1
  20. package/dist/services/mm-api-service/mm-aggregated-part-counts.d.ts +33 -0
  21. package/dist/services/mm-api-service/mm-aggregated-part-counts.d.ts.map +1 -0
  22. package/dist/services/mm-api-service/mm-aggregated-part-counts.js +95 -0
  23. package/dist/services/mm-api-service/mm-aggregated-part-counts.js.map +1 -0
  24. package/dist/services/mm-api-service/mm-api-service.d.ts +49 -1
  25. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  26. package/dist/services/mm-api-service/mm-api-service.js +127 -10
  27. package/dist/services/mm-api-service/mm-api-service.js.map +1 -1
  28. package/dist/services/mm-api-service/mm-graphql-proxy.d.ts +13 -0
  29. package/dist/services/mm-api-service/mm-graphql-proxy.d.ts.map +1 -0
  30. package/dist/services/mm-api-service/mm-graphql-proxy.js +25 -0
  31. package/dist/services/mm-api-service/mm-graphql-proxy.js.map +1 -0
  32. package/dist/services/mm-api-service/token-mgr.d.ts.map +1 -1
  33. package/dist/services/mm-api-service/token-mgr.js +0 -5
  34. package/dist/services/mm-api-service/token-mgr.js.map +1 -1
  35. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts +33 -0
  36. package/dist/services/mm-api-service/types/mm-response-interfaces.d.ts.map +1 -1
  37. package/dist/services/mm-api-service/types/mm-response-interfaces.js.map +1 -1
  38. package/dist/utils/resource-group.d.ts.map +1 -1
  39. package/dist/utils/resource-group.js +13 -1
  40. package/dist/utils/resource-group.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/index.ts +2 -0
  43. package/src/services/data-sync-service/configuration-manager.ts +17 -7
  44. package/src/services/data-sync-service/nats-labor-ticket-listener.ts +23 -2
  45. package/src/services/erp-api-services/errors.ts +4 -0
  46. package/src/services/mm-api-service/graphql-string-literal.ts +11 -0
  47. package/src/services/mm-api-service/index.ts +4 -0
  48. package/src/services/mm-api-service/mm-aggregated-part-counts.ts +127 -0
  49. package/src/services/mm-api-service/mm-api-service.ts +172 -10
  50. package/src/services/mm-api-service/mm-graphql-proxy.ts +33 -0
  51. package/src/services/mm-api-service/token-mgr.ts +0 -5
  52. package/src/services/mm-api-service/types/mm-response-interfaces.ts +35 -0
  53. package/src/utils/resource-group.ts +15 -2
@@ -0,0 +1,127 @@
1
+ /**
2
+ * GraphQL `aggregatedPartCounts` argument builder (inline `args: { ... }` fragment).
3
+ * `machineRef`, `metricKey`, `windowStartAt`, and `windowEndAt` are required in `args`.
4
+ * Optional tuning: `bucketWidth`, `bucketResolution`, and `windowWidth` are passed through when set (see MM GraphQL schema).
5
+ *
6
+ * @see https://developers.machinemetrics.com/docs/graphql/api
7
+ */
8
+
9
+ import type { MMAggregatedPartCountBucket } from "./types/mm-response-interfaces.js";
10
+ import { escapeGraphqlStringLiteral } from "./graphql-string-literal.js";
11
+
12
+ export const MM_DEFAULT_PART_COUNT_METRIC_KEY = "part-count" as const;
13
+
14
+ export function buildAggregatedPartCountsArgsFragment(options: {
15
+ machineRef: number;
16
+ metricKey: string;
17
+ windowStartAt: string;
18
+ windowEndAt: string;
19
+ bucketWidth?: string;
20
+ bucketResolution?: number;
21
+ windowWidth?: string;
22
+ }): string {
23
+ const refInt = Math.trunc(Number(options.machineRef));
24
+ if (!Number.isFinite(refInt)) {
25
+ throw new RangeError("machineRef must be a finite number");
26
+ }
27
+ const parts: string[] = [
28
+ `machineRef: ${refInt}`,
29
+ `metricKey: "${escapeGraphqlStringLiteral(options.metricKey)}"`,
30
+ `windowStartAt: "${escapeGraphqlStringLiteral(options.windowStartAt)}"`,
31
+ `windowEndAt: "${escapeGraphqlStringLiteral(options.windowEndAt)}"`,
32
+ ];
33
+ if (options.bucketWidth !== undefined) {
34
+ parts.push(`bucketWidth: "${escapeGraphqlStringLiteral(options.bucketWidth)}"`);
35
+ }
36
+ if (options.bucketResolution !== undefined) {
37
+ const bucketResolution = Number(options.bucketResolution);
38
+ if (!Number.isFinite(bucketResolution)) {
39
+ throw new RangeError("bucketResolution must be a finite number");
40
+ }
41
+ parts.push(`bucketResolution: ${bucketResolution}`);
42
+ }
43
+ if (options.windowWidth !== undefined) {
44
+ parts.push(`windowWidth: "${escapeGraphqlStringLiteral(options.windowWidth)}"`);
45
+ }
46
+ return parts.join(", ");
47
+ }
48
+
49
+ export function buildAggregatedPartCountsQuery(options: {
50
+ machineRef: number;
51
+ metricKey: string;
52
+ windowStartAt: string;
53
+ windowEndAt: string;
54
+ bucketWidth?: string;
55
+ bucketResolution?: number;
56
+ windowWidth?: string;
57
+ }): string {
58
+ const args = buildAggregatedPartCountsArgsFragment(options);
59
+ return `query aggregatedPartCountsForMachine { aggregatedPartCounts(args: { ${args} }) { machineRef metricKey sumOfValues eventCount bucketStartAt bucketEndAt bucketWidth firstValue lastValue } }`;
60
+ }
61
+
62
+ /**
63
+ * Parses one `aggregatedPartCounts` bucket from JSON.
64
+ * Per MM GraphQL, each returned row includes non-null strings for `metricKey`, `bucketStartAt`, `bucketEndAt`, and `bucketWidth` (width may be empty); absence, null, or wrong types throw.
65
+ */
66
+ export function parseAggregatedPartCountBucketRow(
67
+ raw: unknown
68
+ ): MMAggregatedPartCountBucket {
69
+ if (typeof raw !== "object" || raw === null) {
70
+ throw new Error("aggregatedPartCounts: invalid row");
71
+ }
72
+ const o = raw as Record<string, unknown>;
73
+ const machineRef =
74
+ typeof o.machineRef === "number" ? o.machineRef : Number(o.machineRef);
75
+ const sumOfValues =
76
+ typeof o.sumOfValues === "number" ? o.sumOfValues : Number(o.sumOfValues);
77
+ const eventCount =
78
+ typeof o.eventCount === "number" ? o.eventCount : Number(o.eventCount);
79
+ if (typeof o.metricKey !== "string" || o.metricKey.length === 0) {
80
+ throw new Error("aggregatedPartCounts: invalid or missing metricKey");
81
+ }
82
+ if (typeof o.bucketStartAt !== "string" || o.bucketStartAt.length === 0) {
83
+ throw new Error("aggregatedPartCounts: invalid or missing bucketStartAt");
84
+ }
85
+ if (typeof o.bucketEndAt !== "string" || o.bucketEndAt.length === 0) {
86
+ throw new Error("aggregatedPartCounts: invalid or missing bucketEndAt");
87
+ }
88
+ if (typeof o.bucketWidth !== "string") {
89
+ throw new Error("aggregatedPartCounts: invalid or missing bucketWidth");
90
+ }
91
+ const metricKey = o.metricKey;
92
+ const bucketStartAt = o.bucketStartAt;
93
+ const bucketEndAt = o.bucketEndAt;
94
+ const bucketWidth = o.bucketWidth;
95
+ if (
96
+ !Number.isFinite(machineRef) ||
97
+ !Number.isFinite(sumOfValues) ||
98
+ !Number.isFinite(eventCount)
99
+ ) {
100
+ throw new Error("aggregatedPartCounts: missing numeric fields");
101
+ }
102
+ const firstValue =
103
+ o.firstValue === null || o.firstValue === undefined
104
+ ? null
105
+ : typeof o.firstValue === "number"
106
+ ? o.firstValue
107
+ : Number(o.firstValue);
108
+ const lastValue =
109
+ o.lastValue === null || o.lastValue === undefined
110
+ ? null
111
+ : typeof o.lastValue === "number"
112
+ ? o.lastValue
113
+ : Number(o.lastValue);
114
+ return {
115
+ machineRef,
116
+ metricKey,
117
+ sumOfValues,
118
+ eventCount,
119
+ bucketStartAt,
120
+ bucketEndAt,
121
+ bucketWidth,
122
+ firstValue:
123
+ firstValue === null || Number.isFinite(firstValue) ? firstValue : null,
124
+ lastValue:
125
+ lastValue === null || Number.isFinite(lastValue) ? lastValue : null,
126
+ };
127
+ }
@@ -13,7 +13,19 @@ import { Checkpoint } from "./types/checkpoint.js";
13
13
  import { CoreConfiguration } from "../data-sync-service/configuration-manager.js";
14
14
  import { ErrorHandler } from "../erp-api-services/errors.js";
15
15
  import {
16
+ assertMmGraphqlProxyResponseHasNoErrors,
17
+ buildMmGraphqlProxyRequestBody,
18
+ } from "./mm-graphql-proxy.js";
19
+ import {
20
+ buildAggregatedPartCountsQuery,
21
+ MM_DEFAULT_PART_COUNT_METRIC_KEY,
22
+ parseAggregatedPartCountBucketRow,
23
+ } from "./mm-aggregated-part-counts.js";
24
+ import { escapeGraphqlStringLiteral } from "./graphql-string-literal.js";
25
+ import {
26
+ MMAggregatedPartCountBucket,
16
27
  MMApiBaseResponse,
28
+ MMMappedErpResourceRow,
17
29
  MMGraphQLReasonResponse,
18
30
  MMGraphQLResourceResponse,
19
31
  MMGraphQLWorkOrderResponse,
@@ -344,11 +356,12 @@ export class MMApiClient {
344
356
  variables?: Record<string, unknown>
345
357
  ): Promise<MMApiBaseResponse> {
346
358
  try {
347
- const body: Record<string, unknown> = { operationName, query };
348
- if (variables !== undefined && Object.keys(variables).length > 0) {
349
- body.variables = variables;
350
- }
351
- return await this.postData(
359
+ const body = buildMmGraphqlProxyRequestBody(
360
+ operationName,
361
+ query,
362
+ variables
363
+ );
364
+ const result = (await this.postData(
352
365
  this.resourceURLs[GRAPHQL],
353
366
  body,
354
367
  {},
@@ -357,7 +370,9 @@ export class MMApiClient {
357
370
  token: CoreConfiguration.inst().mmApiAuthToken,
358
371
  headers: { "Content-Type": "application/json" },
359
372
  }
360
- );
373
+ )) as Record<string, unknown> & MMApiBaseResponse;
374
+ assertMmGraphqlProxyResponseHasNoErrors(result);
375
+ return result;
361
376
  } catch (error) {
362
377
  ErrorHandler.handle(error);
363
378
  }
@@ -838,10 +853,7 @@ export class MMApiClient {
838
853
  async fetchWorkOrderFromGraphQL(
839
854
  workOrderId: string
840
855
  ): Promise<MMGraphQLWorkOrderResponse> {
841
- // Escape just enough to safely embed in a GraphQL string literal
842
- const escapedWorkOrderId = workOrderId
843
- .replace(/\\/g, "\\\\")
844
- .replace(/"/g, '\\"');
856
+ const escapedWorkOrderId = escapeGraphqlStringLiteral(workOrderId);
845
857
 
846
858
  const query =
847
859
  `query getErpWorkOrder { ` +
@@ -858,6 +870,127 @@ export class MMApiClient {
858
870
  )) as unknown as MMGraphQLWorkOrderResponse;
859
871
  }
860
872
 
873
+ /**
874
+ * Returns ERP resources for a company that are **mapped to MachineMetrics** (`machineRef` is set).
875
+ * GraphQL: `erpResources` filtered by `companyId` and non-null `machineRef`. Extends
876
+ * {@link fetchResourcesFromGraphQL} with company scope and richer fields (`isResourceGroup`, `machineGroupId`, etc.).
877
+ *
878
+ * @param companyId - MachineMetrics company identifier (string). Whitespace is trimmed; empty after trim throws.
879
+ * @returns Parsed rows; order matches the API response.
880
+ * @throws {Error} When `companyId` is missing, the response shape is invalid, or the GraphQL proxy returns top-level `errors`.
881
+ */
882
+ async fetchMappedErpResourcesForCompany(
883
+ companyId: string
884
+ ): Promise<MMMappedErpResourceRow[]> {
885
+ const id = companyId.trim();
886
+ if (!id) {
887
+ throw new Error("fetchMappedErpResourcesForCompany: companyId is required");
888
+ }
889
+ const safe = escapeGraphqlStringLiteral(id);
890
+ const query = `query mappedErpResourcesForCompany { erpResources(where: { companyId: { _eq: "${safe}" }, machineRef: { _is_null: false } }) { isResourceGroup machineGroupId machineRef resourceId name type } }`;
891
+ const response = (await this.graphqlRequest(
892
+ "mappedErpResourcesForCompany",
893
+ query
894
+ )) as { data?: { erpResources?: unknown } };
895
+ const list = response.data?.erpResources;
896
+ if (!Array.isArray(list)) {
897
+ throw new Error(
898
+ "fetchMappedErpResourcesForCompany: missing data.erpResources array"
899
+ );
900
+ }
901
+ return list.map(parseMappedErpResourceRow);
902
+ }
903
+
904
+ /**
905
+ * GraphQL `aggregatedPartCounts` for one `machineRef`: **one HTTP request**; the API may return **multiple bucket rows**
906
+ * (time buckets) for that machine.
907
+ *
908
+ * @param options.machineRef - Numeric MM machine reference (`machineRef` in GraphQL).
909
+ * @param options.windowStartAt - Window start as an ISO-8601 string (e.g. `2025-01-01T00:00:00.000Z`). Must match MM GraphQL expectations.
910
+ * @param options.windowEndAt - Window end, same format as `windowStartAt`.
911
+ * @param options.metricKey - Metric to aggregate; defaults to `"part-count"` (`MM_DEFAULT_PART_COUNT_METRIC_KEY`).
912
+ * @param options.bucketWidth - Optional. GraphQL duration string for bucket size (API-specific; e.g. hour/day windowing).
913
+ * @param options.bucketResolution - Optional. Numeric resolution when the API supports sub-bucket refinement.
914
+ * @param options.windowWidth - Optional. Alternative window sizing hint when supported by the schema.
915
+ * @returns One row per bucket from `data.aggregatedPartCounts`.
916
+ * @throws {Error} When the response lacks `data.aggregatedPartCounts`, a row fails parsing, or GraphQL returns errors.
917
+ * @throws {RangeError} When `machineRef` or `bucketResolution` is not finite (via query builder).
918
+ *
919
+ * For multiple machines, call once per `machineRef` (for example `Promise.all`) so filtering and error policy stay explicit at the call site.
920
+ */
921
+ async fetchAggregatedPartCountsForMachine(options: {
922
+ machineRef: number;
923
+ windowStartAt: string;
924
+ windowEndAt: string;
925
+ metricKey?: string;
926
+ bucketWidth?: string;
927
+ bucketResolution?: number;
928
+ windowWidth?: string;
929
+ }): Promise<MMAggregatedPartCountBucket[]> {
930
+ const metricKey =
931
+ options.metricKey?.trim() || MM_DEFAULT_PART_COUNT_METRIC_KEY;
932
+ const query = buildAggregatedPartCountsQuery({
933
+ machineRef: options.machineRef,
934
+ metricKey,
935
+ windowStartAt: options.windowStartAt,
936
+ windowEndAt: options.windowEndAt,
937
+ bucketWidth: options.bucketWidth,
938
+ bucketResolution: options.bucketResolution,
939
+ windowWidth: options.windowWidth,
940
+ });
941
+ const response = (await this.graphqlRequest(
942
+ "aggregatedPartCountsForMachine",
943
+ query
944
+ )) as { data?: { aggregatedPartCounts?: unknown } };
945
+ const list = response.data?.aggregatedPartCounts;
946
+ if (!Array.isArray(list)) {
947
+ throw new Error(
948
+ "fetchAggregatedPartCountsForMachine: missing data.aggregatedPartCounts array"
949
+ );
950
+ }
951
+ return list.map(parseAggregatedPartCountBucketRow);
952
+ }
953
+
954
+ /**
955
+ * Resolves the REST **`machine_id`** for **part adjustment** and other MM REST calls that take
956
+ * `machine_id`, using GraphQL `machines` where `machineRef` matches. Docs often describe this as a UUID;
957
+ * this method does not validate a particular string format—only that the GraphQL field is a non-empty string.
958
+ *
959
+ * @param machineRef - Numeric MM `machineRef`. Non-finite values throw.
960
+ * @returns Trimmed `machine_id` string, or `null` if no row, unmapped machine, or when `machineId` is
961
+ * missing, empty after trim, or not a string (avoids coercing arbitrary JSON values to strings).
962
+ * @throws {RangeError} When `machineRef` is not finite.
963
+ * @throws {Error} When GraphQL returns errors.
964
+ */
965
+ async fetchMachineIdForMachineRef(
966
+ machineRef: number
967
+ ): Promise<string | null> {
968
+ const ref = Math.trunc(Number(machineRef));
969
+ if (!Number.isFinite(ref)) {
970
+ throw new RangeError("fetchMachineIdForMachineRef: machineRef must be finite");
971
+ }
972
+ const query = `query mmMachineIdByRef { machines(where: { machineRef: { _eq: ${ref} } }, limit: 1) { machineId } }`;
973
+ const response = (await this.graphqlRequest(
974
+ "mmMachineIdByRef",
975
+ query
976
+ )) as { data?: { machines?: unknown } };
977
+ const list = response.data?.machines;
978
+ if (!Array.isArray(list) || list.length === 0) {
979
+ return null;
980
+ }
981
+ const row = list[0];
982
+ if (typeof row !== "object" || row === null) {
983
+ return null;
984
+ }
985
+ const o = row as Record<string, unknown>;
986
+ const mid = o.machineId;
987
+ if (typeof mid !== "string") {
988
+ return null;
989
+ }
990
+ const s = mid.trim();
991
+ return s || null;
992
+ }
993
+
861
994
  /**
862
995
  * Cleanup all HTTP connections and resources
863
996
  * Call this when the service is no longer needed
@@ -869,3 +1002,32 @@ export class MMApiClient {
869
1002
 
870
1003
  //#endregion public methods
871
1004
  }
1005
+
1006
+ function parseMappedErpResourceRow(raw: unknown): MMMappedErpResourceRow {
1007
+ if (typeof raw !== "object" || raw === null) {
1008
+ throw new Error("fetchMappedErpResourcesForCompany: invalid row");
1009
+ }
1010
+ const o = raw as Record<string, unknown>;
1011
+ const refRaw = o.machineRef;
1012
+ const ref = typeof refRaw === "number" ? refRaw : Number(refRaw);
1013
+ if (!Number.isFinite(ref)) {
1014
+ throw new Error("fetchMappedErpResourcesForCompany: invalid machineRef");
1015
+ }
1016
+ const rid = o.resourceId;
1017
+ return {
1018
+ isResourceGroup: Boolean(o.isResourceGroup),
1019
+ machineGroupId:
1020
+ o.machineGroupId === null || o.machineGroupId === undefined
1021
+ ? null
1022
+ : String(o.machineGroupId),
1023
+ machineRef: Math.trunc(ref),
1024
+ resourceId:
1025
+ rid === null || rid === undefined
1026
+ ? null
1027
+ : typeof rid === "string"
1028
+ ? rid.trim() || null
1029
+ : String(rid).trim() || null,
1030
+ name: typeof o.name === "string" ? o.name : null,
1031
+ type: o.type === null || o.type === undefined ? null : String(o.type),
1032
+ };
1033
+ }
@@ -0,0 +1,33 @@
1
+ import { GraphQLError } from "../erp-api-services/errors.js";
2
+ import type { GraphQLServerError } from "../erp-api-services/graphql/types.js";
3
+
4
+ /**
5
+ * JSON body for POST to MM API `/proxy/graphql` (MachineMetrics GraphQL proxy).
6
+ * Used only by `MMApiClient` (not exported from the public SDK entrypoint).
7
+ */
8
+ export function buildMmGraphqlProxyRequestBody(
9
+ operationName: string,
10
+ query: string,
11
+ variables?: Record<string, unknown>
12
+ ): Record<string, unknown> {
13
+ const body: Record<string, unknown> = { operationName, query };
14
+ if (variables !== undefined && Object.keys(variables).length > 0) {
15
+ body.variables = variables;
16
+ }
17
+ return body;
18
+ }
19
+
20
+ /**
21
+ * GraphQL-over-HTTP often returns **HTTP 200** with a top-level `errors` array and partial or null `data`.
22
+ * Axios treats that as success, so callers must inspect the envelope.
23
+ *
24
+ * @throws {GraphQLError} when `errors` is a non-empty array
25
+ */
26
+ export function assertMmGraphqlProxyResponseHasNoErrors(
27
+ response: Record<string, unknown>
28
+ ): void {
29
+ const errors = response.errors;
30
+ if (Array.isArray(errors) && errors.length > 0) {
31
+ throw new GraphQLError("GraphQL Error", errors as GraphQLServerError[]);
32
+ }
33
+ }
@@ -24,12 +24,7 @@ export class MMTokenManager {
24
24
  ? new Date(cached.expiration)
25
25
  : null;
26
26
  const config = CoreConfiguration.inst();
27
- console.log("=== MMTOKEN MANAGER DEBUG ===");
28
- console.log("config.mmApiBaseUrl:", config.mmApiBaseUrl);
29
- console.log("config object keys:", Object.keys(config));
30
27
  this.baseUrl = config.mmApiBaseUrl;
31
- console.log("this.baseUrl set to:", this.baseUrl);
32
- console.log("=== END MMTOKEN MANAGER DEBUG ===");
33
28
  this.api = HTTPClientFactory.getInstance({
34
29
  baseUrl: this.baseUrl,
35
30
  retryAttempts: config.mmApiRetryAttempts,
@@ -331,6 +331,41 @@ export interface MMGraphQLWorkOrderResponse extends MMApiBaseResponse {
331
331
  };
332
332
  }
333
333
 
334
+ /**
335
+ * ERP resources for a company with a mapped MM machine (`machineRef` present).
336
+ */
337
+ export interface MMMappedErpResourceRow {
338
+ isResourceGroup: boolean;
339
+ machineGroupId: string | null;
340
+ machineRef: number;
341
+ /** ERP resource id when exposed by GraphQL (connector-specific meaning). */
342
+ resourceId: string | null;
343
+ /** Display name from GraphQL; null when absent or not a string (callers may default for UI). */
344
+ name: string | null;
345
+ type: string | null;
346
+ }
347
+
348
+ /**
349
+ * One time bucket row from GraphQL `aggregatedPartCounts` for a single `machineRef` and `metricKey`.
350
+ * Window and bucket shape follow the MM GraphQL schema (see MachineMetrics GraphQL docs).
351
+ */
352
+ export interface MMAggregatedPartCountBucket {
353
+ machineRef: number;
354
+ metricKey: string;
355
+ /** Sum of metric values across events in this bucket. */
356
+ sumOfValues: number;
357
+ /** Number of contributing events in this bucket. */
358
+ eventCount: number;
359
+ bucketStartAt: string;
360
+ bucketEndAt: string;
361
+ /** Duration string for this bucket when the API provides it (may be empty). */
362
+ bucketWidth: string;
363
+ /** Optional gauge at bucket start; null when not applicable. */
364
+ firstValue: number | null;
365
+ /** Optional gauge at bucket end; null when not applicable. */
366
+ lastValue: number | null;
367
+ }
368
+
334
369
  /**
335
370
  * Response for machine groups fetch
336
371
  * Used by fetchMachineGroups() method
@@ -1,4 +1,5 @@
1
1
  import { sdkLogger as logger } from "../services/reporting-service/logger.js";
2
+ import { GraphQLError } from "../services/erp-api-services/errors.js";
2
3
  import {
3
4
  MMApiClient,
4
5
  MMGraphQLResourceResponse,
@@ -22,8 +23,20 @@ export async function getResourceERPGroupId(
22
23
  ): Promise<string | undefined> {
23
24
  try {
24
25
  // Get the ERP resources to map resource IDs to machine refs
25
- const erpResources =
26
- (await client.fetchResourcesFromGraphQL()) as MMGraphQLResourceResponse;
26
+ let erpResources: MMGraphQLResourceResponse;
27
+ try {
28
+ erpResources =
29
+ (await client.fetchResourcesFromGraphQL()) as MMGraphQLResourceResponse;
30
+ } catch (error) {
31
+ if (error instanceof GraphQLError) {
32
+ logger.warn(
33
+ "ERP resources GraphQL request failed; cannot resolve resource group",
34
+ { resourceId, graphqlErrors: error.errors }
35
+ );
36
+ return undefined;
37
+ }
38
+ throw error;
39
+ }
27
40
  if (!erpResources?.data?.machineResource) {
28
41
  logger.error("Failed to fetch ERP resources");
29
42
  return undefined;