@machinemetrics/mm-erp-sdk
v0.9.8
Published
A library for syncing data between MachineMetrics and ERP systems
Keywords
Readme
MM ERP Connector SDK
A TypeScript SDK for building ERP connectors that integrate with MachineMetrics' data synchronization platform.
Overview
This SDK provides the core infrastructure for:
- Data Sync Service: Automated job scheduling for bidirectional ERP data synchronization
- ERP Connector Interface: Standardized interface for implementing ERP connectors
- MM API Integration: Client for interacting with MachineMetrics APIs
- Timezone handling: Company timezone cached at startup; conversion helpers for the ERP boundary (see Timezone handling)
- Utility Functions: Common data transformation and HTTP utilities
Documentation
- Building a new connector? Start with the MachineMetrics ERP connector guide on the developer hub — it orients you across scaffolding, implementation, and deployment.
- SDK API detail (this repo):
CONNECTOR_SDK_UTILITIES.mdfor the SDK classes/services/utilities, andMM_ENTITY_KEYS_REFERENCE.mdfor entity keys and referential integrity. - Connector implementation workflow: the
generic-erp-connectortemplate'sCONNECTOR-GUIDE.md(how a generated project wires the SDK together).
Quick Start
1. Install the SDK
npm install @machinemetrics/mm-erp-sdk2. Implement an ERP Connector
import path from "path";
import { fileURLToPath } from "url";
import {
IERPConnector,
IERPLaborTicketHandler,
ERPObjType,
MMReceiveLaborTicket,
RecoverLaborTicketIdAction,
RecoverLaborTicketIdReturn,
ApplicationInitializer,
runDataSyncService,
} from "@machinemetrics/mm-erp-sdk";
export default class MyERPConnector
implements IERPConnector, IERPLaborTicketHandler
{
get type(): string {
return "JOB_BOSS"; // replace with your ERP type
}
async startUp(): Promise<void> {
try {
await ApplicationInitializer.initialize();
// Bree workers re-import the built connector module from disk each cycle,
// so runDataSyncService needs the path to this connector's compiled JS.
const connectorPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"my-erp-connector.js"
);
await runDataSyncService(connectorPath);
} catch (error) {
console.error("Startup failed:", error);
process.exitCode = 1;
}
}
async syncFromERP(): Promise<void> {
// Implement your data sync from ERP logic
}
async syncToERP(): Promise<void> {
// Implement your data sync to ERP logic
}
async createLaborTicketInERP(
laborTicket: MMReceiveLaborTicket
): Promise<{ laborTicket: MMReceiveLaborTicket; erpUid: string }> {
// Implement labor ticket creation
}
// Optional: recover an existing ERP laborTicketId before create when MM lost the link
async recoverLaborTicketId(
laborTicket: MMReceiveLaborTicket
): Promise<RecoverLaborTicketIdReturn> {
return { action: RecoverLaborTicketIdAction.CREATE };
}
async updateLaborTicketInERP(
laborTicket: MMReceiveLaborTicket
): Promise<MMReceiveLaborTicket> {
// Implement labor ticket updates
}
// Implement other required methods...
}3. Start the Data Sync Service
import MyERPConnector from "./my-erp-connector";
const connector = new MyERPConnector();
await connector.startUp();
runDataSyncService(connectorPath)requires a path to the connector's compiled JS module (not the.tssource). See the templateCONNECTOR-GUIDE.mdfor the build/runtime path resolution details.
Data Sync Jobs
The SDK includes automated jobs that run on configurable intervals:
- from-erp: Syncs data from ERP to MachineMetrics
- to-erp: Syncs labor tickets and employee timesheets from MachineMetrics to ERP
- retry-failed-labor-tickets: Retries failed labor ticket operations
- clean-up-expired-cache: Maintains cache hygiene
Configure job intervals via environment variables (Bree-compatible strings, e.g. "5 min", "1h"):
FROM_ERP_INTERVAL="5 min"
TO_ERP_INTERVAL="5 min"
RETRY_LABOR_TICKETS_INTERVAL="30 min"
CACHE_EXPIRATION_CHECK_INTERVAL="60 min"Execution model
Jobs are scheduled with Bree. Each job cycle runs as an independent worker process that constructs a fresh connector instance — so in-memory state does not persist across cycles, and cycles are not guaranteed to run serially (labor tickets may be processed concurrently within a cycle). Persist durable state in the SDK's SQLite stores or MM checkpoints, not in instance/module memory. See CONNECTOR_SDK_UTILITIES.md (runDataSyncService) for the full contract.
Labor Ticket Synchronization
The SDK provides bidirectional labor ticket synchronization with built-in protection against circular writes (ERP → MM → ERP):
- Labor sync ledger (SQLite-backed): records a per-ticket business signature so the
to-erpexport skips tickets whose business-relevant content is unchanged since they were last processed. Tickets imported from the ERP before MM assigns alaborTicketRefare tracked under a stableerp-import:{workOrderId}:{laborTicketId}key. - ERP labor ticket ID recovery (optional): connectors may implement
recoverLaborTicketIdonIERPLaborTicketHandlerto look up an existing ERP row before create when MM has nolaborTicketId. When recovery returns an id (RECOVER), polling and NATS patch MM then callupdateLaborTicketInERP. When recovery returnsDEFER, the SDK skips create for that cycle and retries later. Duplicate prevention depends on the connector implementing recovery correctly; connectors without the hook are unchanged. - Partial ERP create (optional): when
createLaborTicketInERPfails but an ERP id was assigned, throwLaborTicketCreatePartialSuccessErrorso the SDK patches MM before rethrowing. SeeCONNECTOR_SDK_UTILITIES.md. - ERP → MM import filtering:
StandardProcessDrivers.writeLaborTicketsToMmFromErpImport()applies MES/operator allowlists and FK rules, pre-marks accepted tickets in the ledger, writes them to MM, and advances the labor-tickets export checkpoint. - Hashed batch cache:
BatchCacheManager/StandardProcessDrivers.writeEntitiesToMM()deduplicate repeated upserts within a batch (local + API-side). - Signature semantics: the business signature excludes identity fields (
laborTicketRef, ERP-import keys) and SDK-mutated fields, so the same business state produces a stable hash across import and the next export. A dashboard edit changes the signature, so the change is exported.
See CONNECTOR_SDK_UTILITIES.md for the helper APIs, and the template CONNECTOR-GUIDE.md for the connector workflow.
Persistent Storage
The SDK persists durable state — the labor sync ledger, the dedup/batch cache, and SDK runtime state — in a SQLite database at the path given by SQLITE_DB_PATH (default ./local.sqlite3):
SQLITE_DB_PATH=/app/data/local.sqlite3This file must survive process/container restarts for the sync ledger and dedup cache to remain correct across cycles. How that path is made durable (e.g. a mounted volume or backed-up disk) is a deployment concern owned by your connector's deployment setup, not by the SDK.
Timesheet Export
The SDK provides employee clock in/out export from MM to the ERP, separate from shop-floor labor tickets:
IERPTimesheetHandler— connector-implemented hook that receivesMMReceiveTimesheetrows.StandardProcessDrivers.syncTimesheetsToERP()— fetches pending rows from/erp/v1/timesheets/updates, processes them intimesheetReforder, and checkpoints progress. It assumes labor ticket export has already run for the cycle, so shop-floor labor is written/closed before clock-out.
Timezone handling
MachineMetrics exchanges datetimes as ISO-8601 strings in UTC (trailing Z or an explicit +00:00 offset). The ERP boundary is where timezone conversion happens.
Cached timezone (no connector fetch)
ApplicationInitializer.initialize() fetches company info from MM (/accounts/current) and persists it in SQLite runtime state, including the company's IANA timezone name. After initialization succeeds, connector code reads it synchronously — no extra HTTP, no ERP-specific timezone configuration:
getCachedTimezoneName()/getERPTimezone()— IANA name (e.g.America/New_York)getCachedTimezoneOffset()— current offset in hours for that zone (DST-aware)
Connectors do not fetch or configure a timezone. Call ApplicationInitializer.initialize() once at startup; the conversion helpers use the cached zone by default.
MM company timezone vs ERP server timezone
The cached value is the MM company's timezone (company.timezone from /accounts/current). The SDK's default assumption is that ERP-local wall time matches the MM company timezone. If your ERP server's clock uses a different zone, pass an explicit timezone? argument to the conversion helpers.
Do not treat "ERP timezone" and "company timezone" as separate inputs the connector must obtain — the SDK already provides the company zone unless you need to override it.
When to convert (depends on sync direction)
| Path | SDK converts? | Connector responsibility |
|------|---------------|--------------------------|
| MM → ERP labor tickets (syncLaborTicketsToERP) | Yes — UTC → ERP-local before your handler runs | Treat clockIn, clockOut, etc. as localized wall time. Do not apply further timezone shifts. |
| MM → ERP timesheets (syncTimesheetsToERP) | No | If the ERP expects local datetimes, call convertUtcDateTimeToErpLocal() in your handler. |
| ERP → MM (writeEntitiesToMM, writeLaborTicketsToMmFromErpImport) | No | Convert ERP-local datetimes to UTC with convertErpLocalDateTimeToUtc() before passing records to the SDK. |
Conversion is field-specific: apply it only where the ERP field semantics are local wall time. If an ERP field is already stored in UTC, converting would corrupt the value.
Conversion helpers
All are exported from the package root. Inputs and outputs are ISO-8601 strings; invalid input throws.
import {
getERPTimezone,
convertUtcDateTimeToErpLocal,
convertErpLocalDateTimeToUtc,
} from "@machinemetrics/mm-erp-sdk";
// After ApplicationInitializer.initialize():
const zone = getERPTimezone(); // cached MM company IANA name
// MM UTC → ERP-local (e.g. timesheets, or custom MM→ERP paths)
const localIso = convertUtcDateTimeToErpLocal("2024-06-15T18:30:00.000Z");
// ERP-local → MM UTC (required before writeEntitiesToMM)
const utcIso = convertErpLocalDateTimeToUtc("2024-06-15T14:30:00");
// Override when ERP clock differs from MM company timezone:
convertErpLocalDateTimeToUtc(localIso, "Europe/Amsterdam");Offset-based formatting helpers (formatDateWithTZOffset, toISOWithOffset, convertToLocalTime, applyTimezoneOffsetsToFields) remain available for legacy ERP formats; prefer the Luxon-based convert* helpers for new code.
See CONNECTOR_SDK_UTILITIES.md for full helper detail.
ERP Resource Mapping (MES Machine Settings)
Machine-to-ERP-resource mapping is sourced from MES machine settings (GET /mes/settings/machine-settings):
MMApiClient.fetchMappedErpResourcesForCompany()— machines mapped to ERP labor resources (machineRef+laborResourceId), sourced from MES machine settings.MMApiClient.fetchMesSchedulableErpResourceIds()/getMesLaborImportResourceIds()— schedulable and labor-import resource-id allowlists.
See CONNECTOR_SDK_UTILITIES.md for MES machine-settings helpers and MM_ENTITY_KEYS_REFERENCE.md for entity keys.
Environment Configuration
These are the environment variables the SDK reads (via CoreConfiguration, getSQLServerConfiguration, getErpApiConnectionParams, and the SQLite/knex setup). Names and defaults below match the SDK source; defaults are shown where one exists.
Required
# MM REST API base URL (e.g. https://api.machinemetrics.com). The SDK throws on startup if this is unset.
MM_MAPPING_AUTH_SERVICE_URL="https://api.machinemetrics.com"
# MM company auth token. ApplicationInitializer.initialize() fetches /accounts/current with this
# bearer token. If unset, startup retries every 10s (up to 36000 attempts) and initialization
# never completes — set this before calling initialize().
MM_MAPPING_SERVICE_TOKEN="your-company-auth-token"
# MM ERP mapping service REST API URL (e.g. https://erp-api.svc.machinemetrics.com). Required for
# entity sync, labor-ticket export, checkpoints, and other /erp/v1/* calls via MMApiClient.
MM_MAPPING_SERVICE_URL="https://erp-api.svc.machinemetrics.com"MachineMetrics API
MM_API_TIMEOUT_SEC="30" # MM API request timeout, seconds (default: 30)
MM_API_RETRY_ATTEMPTS="0" # MM API retry attempts (default: 0)General
NODE_ENV="production" # Runtime environment (default: "development")
LOG_LEVEL="info" # Log level (default: "info")
SDK_LOG_PREFIX="SDK:" # Prefix applied to SDK log lines (default: "SDK:")
NATS_ENABLED="false" # When "true", starts the real-time NATS labor-ticket listener alongside Bree jobsJob intervals
Interval strings accept any Bree-compatible duration (e.g. "5 min", "1h").
FROM_ERP_INTERVAL="5 min" # from-ERP sync interval (default: "5 min", or POLL_INTERVAL if set)
TO_ERP_INTERVAL="5 min" # to-ERP sync interval (default: "5 min")
RETRY_LABOR_TICKETS_INTERVAL="30 min" # retry-failed-labor-tickets interval (default: "30 min")
CACHE_EXPIRATION_CHECK_INTERVAL="60 min" # expired-cache cleanup interval (default: "60 min")
POLL_INTERVAL="5 min" # fallback default for FROM_ERP_INTERVAL (default: "")Caching & local storage
CACHE_TTL="604800" # Dedup cache TTL, seconds (default: 604800 = 7 days)
SQLITE_DB_PATH="/app/data/local.sqlite3" # SQLite file for ledger/cache/runtime state (default: "./local.sqlite3")ERP API (generic REST/GraphQL)
ERP_PAGINATION_LIMIT="0" # Default pagination limit for ERP API (default: 0)
ERP_API_TIMEOUT_SEC="30" # ERP API request timeout, seconds (default: 30)
ERP_API_RETRY_ATTEMPTS="3" # ERP API retry attempts (default: 3)Connectors using getErpApiConnectionParams() also read:
ERP_API_URL="https://erp.example.com"
ERP_API_CLIENT_ID="client-id"
ERP_API_CLIENT_SECRET="client-secret"
ERP_API_ORGANIZATION_ID="org-id"
ERP_AUTH_BASE_URL="https://auth.example.com"SQL Server ERP (when using SqlServerService / getSQLServerConfiguration())
ERP_SQLSERVER_USERNAME="username"
ERP_SQLSERVER_PASSWORD="password"
ERP_SQLSERVER_DATABASE="your-db"
ERP_SQLSERVER_HOST="localhost" # alias: ERP_SQLSERVER_SERVER
ERP_SQLSERVER_PORT="1433" # default: 1433
ERP_SQLSERVER_CONNECTION_TIMEOUT="30000" # default: 30000 (ms)
ERP_SQLSERVER_REQUEST_TIMEOUT="60000" # default: 60000 (ms)
ERP_SQLSERVER_MAX="10" # connection pool max (default: 10)
ERP_SQLSERVER_MIN="0" # connection pool min (default: 0)
ERP_SQLSERVER_IDLE_TIMEOUT_MMILLIS="30000" # pool idle timeout, ms (env name spelled as in the SDK; default: 30000)
ERP_SQLSERVER_ENCRYPT="false" # default: false
ERP_SQLSERVER_TRUST_SERVER="false" # default: falseLogging Reliability
- The SDK logger now captures rotate/write transport failures internally, but callers should still treat logging as best-effort. If you manage your own in-process scheduler (e.g., not using Bree), wrap any
job.isRunning = trueflags and subsequentlogger.*calls in atry/finallyblock so the scheduler state clears even if logging throws. - Bree-based connectors inherit process isolation, but custom schedulers run inside a single event loop. Always reset locks/timers inside
finallyclauses to avoid getting stuck when a synchronous dependency (logging, metrics, etc.) fails mid-cycle.
API Reference
Core Interfaces
IERPConnector: Main connector interface (syncFromERP,syncToERP, lifecycle hooks). Thetypegetter is the ERP system identifier passed to MM APIsystemparameters andStandardProcessDriversasconnectorType.IERPLaborTicketHandler: Create/update labor tickets in the ERP (MMReceiveLaborTicket). OptionalrecoverLaborTicketId(RecoverLaborTicketIdAction) when MM lost the ERP link — see Labor Ticket Synchronization andCONNECTOR_SDK_UTILITIES.md.IERPTimesheetHandler: Process employee clock in/out rows in the ERP (MMReceiveTimesheet)
Utilities
ApplicationInitializer: SDK initialization (configuration, migrations, company info including timezone)runDataSyncService: Start automated sync jobsgetCachedCompanyInfo()/getCachedCompanyId()/getCachedCompanyLocationRef()/getCachedTimezoneName(): read company info persisted at startupgetERPTimezone()/convertUtcDateTimeToErpLocal()/convertErpLocalDateTimeToUtc(): datetime boundary conversion (see Timezone handling)formatDateWithTZOffset()/toISOWithOffset()/convertToLocalTime()/applyTimezoneOffsetsToFields(): offset-based formatting for legacy ERP formatsStandardProcessDrivers: Common sync operations, including:syncLaborTicketsToERP(connectorType, handler, { onlyClosed? })syncTimesheetsToERP(connectorType, handler)retryFailedLaborTickets(connectorType, handler)writeLaborTicketsToMmFromErpImport(tickets, filterOptions, cache?, writeOptions?)writeEntitiesToMM(entityType, records, cache?, options?)
Services
SqlServerService/SqlServerHelper: SQL Server ERP integrationPsqlService: PSQL / Pervasive (ODBC) ERP integration, withformatPsqlDate,formatPsqlTime,combinePsqlDateTime,isPsqlDateEmpty,cleanPsqlCharFieldhelpersRestAPIService/GraphQLService: ERP REST / GraphQL API clientsMMApiClient: MachineMetrics API client (includesfetchMappedErpResourcesForCompany,fetchMesSchedulableErpResourceIds,fetchTimesheetUpdates)
For SDK helper, type, and entity-key detail, see CONNECTOR_SDK_UTILITIES.md and MM_ENTITY_KEYS_REFERENCE.md. For the connector implementation workflow, see the template CONNECTOR-GUIDE.md.
Labor ticket updates: the onlyClosed option
syncLaborTicketsToERP accepts an optional onlyClosed flag:
onlyClosed: true— the SDK requests only closed labor tickets, forwardingonlyClosed=trueto/erp/v1/labor-tickets/export/updates.- omitted (default) — all labor tickets are processed.
StandardProcessDrivers.syncLaborTicketsToERP(connectorType, handler, { onlyClosed?: boolean });