The sync process happens in two main phases: fetching BOM data and syncing to the database.
File: apps/erp/app/routes/api+/integrations.onshape.d.$did.v.$vid.e.$eid.bom.ts
- Calls the Onshape API to get the Bill of Materials for a document/version/element
- Transforms raw BOM rows into structured objects with headers as keys
- Looks up existing items by building unique part numbers using
readableIdWithRevision(part number + revision, e.g., "WIDGET-001-A") - Queries the
itemtable to find matches:client.from("item") .select("id, readableId, readableIdWithRevision, defaultMethodType, replenishmentSystem") .in("readableIdWithRevision", Array.from(uniquePartNumbers))
- For each row, attaches:
id: The existing Carbon item ID if found, otherwiseundefined- Preserves existing
replenishmentSystemanddefaultMethodTypeif the item exists - Falls back to Onshape's "Purchasing Level" field for new items ("Purchased" → Buy/Pick, otherwise → Make)
File: packages/database/supabase/functions/sync/index.ts
The sync function processes BOM data in a transaction and handles existing vs new parts differently:
if (itemId) {
// Update existing item with Onshape data
await trx
.updateTable("item")
.set({
externalId: { onshapeData: data.data },
updatedBy: userId,
updatedAt: new Date().toISOString(),
})
.where("id", "=", itemId)
.execute();
}What happens:
- The item record is updated (not replaced) with the Onshape metadata stored in
externalId.onshapeData - The
updatedByandupdatedAtfields are updated - No other fields are changed — the item's name, readableId, revision, replenishment system, etc. are preserved
// Create new item and part
const item = await trx.insertInto("item").values({
readableId: partId,
revision: revision ?? "0",
name,
type: "Part",
unitOfMeasureCode: "EA",
itemTrackingType: "Inventory",
replenishmentSystem,
defaultMethodType,
companyId,
externalId: { onshapeData: data.data },
createdBy: userId,
});
// Create or update the part record
await trx.insertInto("part").values({ id: partId, companyId, createdBy: userId })
.onConflict((oc) => oc.columns(["id", "companyId"]).doUpdateSet({
updatedBy: userId,
updatedAt: new Date().toISOString(),
}));What happens:
- A new
itemrecord is created - A new
partrecord is created (or updated if the part ID already exists) - The item is tracked in a
newlyCreatedItemsByPartIdmap to prevent duplicates within the same sync
For all items (new or existing):
-
Method Materials are deleted and recreated: The function first deletes all existing
methodMaterialentries for the parent make method, then recreates them with the synced data:await trx.deleteFrom("methodMaterial").where("makeMethodId", "=", makeMethodId).execute();
-
Make Methods are created if needed: If an item has children (sub-assemblies) or
defaultMethodType === "Make", amakeMethodis created (or looked up if it exists) -
Tree traversal: The BOM is processed as a tree using dotted index notation (e.g., "1", "1.1", "1.1.1"), ensuring parents are processed before children
The API route (integrations.onshape.sync.ts:76-92) updates the parent item's externalId with sync metadata:
currentExternalId["onshape"] = {
documentId,
versionId,
elementId,
lastSyncedAt: new Date().toISOString()
};Existing parts preserve their core data (readableId, revision, name, replenishmentSystem, defaultMethodType) — only the externalId.onshapeData metadata is updated. The method materials (BOM relationships) are fully recreated to match the current Onshape structure.