/** * Small helpers shared by the MM GraphQL proxy reads/writes in `MMApiClient` * (paging, chunking, numeric coercion of Hasura's varied renderings). */ /** Default rows per GraphQL page for the paged MM proxy reads. */ export const DEFAULT_GRAPHQL_PAGE_SIZE = 500; /** A GraphQL document plus the operation name and variables to post with it. */ export interface GraphqlOperation { operationName: string; query: string; variables?: Record; } /** * Preserve null: MM's numeric columns are nullable and "not provided" must stay * distinguishable from a real 0 for connector-side selection rules. Non-finite * values (NaN, Infinity, unparsable strings) are reported as null too. */ export function toNullableFiniteNumber(value: unknown): number | null { if (value === null || value === undefined || value === "") { return null; } if (typeof value !== "number" && typeof value !== "string") { return null; } const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } /** * A page/chunk size option: finite values of at least 1 are floored, anything else falls * back. The `>= 1` bound matters — a fractional size such as `0.5` floors to `0`, and a * page size of `0` makes `rows.length < pageSize` false for an empty page while * `offset += pageSize` leaves the offset unchanged, so the paged reads would loop forever. */ export function normalizePositiveInteger( value: number | undefined, fallback: number ): number { return value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback; } /** * Split a list into chunks of `chunkSize`. * @throws {RangeError} when `chunkSize` is not a positive finite number — callers normalize * options with {@link normalizePositiveInteger} first; a silent "one chunk of everything" * would put thousands of ids into a single `_in` list. */ export function chunkList(items: readonly T[], chunkSize: number): T[][] { if (!Number.isFinite(chunkSize) || chunkSize < 1) { throw new RangeError( `chunkList: chunkSize must be a positive number, got ${chunkSize}` ); } const size = Math.floor(chunkSize); const chunks: T[][] = []; for (let i = 0; i < items.length; i += size) { chunks.push(items.slice(i, i + size)); } return chunks; } /** * Strings and finite numbers, trimmed; blanks and anything else dropped; deduped **exactly** * (MM compares text exactly); keep * first-seen order. Accepts an optional single value plus an optional list. */ export function collectNonBlankStrings( single: unknown, many: readonly unknown[] | undefined ): string[] { const values = new Set(); const add = (value: unknown) => { // ERP ids are often numeric; the send model stringifies them, so filters must too, or // a numeric work order id would silently filter to "no rows". const text = typeof value === "string" ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : null; if (text === null) return; const trimmed = text.trim(); if (trimmed !== "") values.add(trimmed); }; add(single); // A string is iterable, so a caller outside TypeScript passing `scopeResourceIds: "CELL-A"` // would otherwise become the five values C, E, L, -, A — a silent wrong query. const list: readonly unknown[] = Array.isArray(many) ? many : many === undefined || many === null ? [] : [many]; for (const value of list) add(value); return [...values]; }