Onshape API types + load workflow rewrite + part-number search - #34
Open
AlexKempen wants to merge 47 commits into
Open
Onshape API types + load workflow rewrite + part-number search#34AlexKempen wants to merge 47 commits into
AlexKempen wants to merge 47 commits into
Conversation
Replace `any` in the configuration endpoints and parser with generated Onshape API types, using @hey-api/openapi-ts's parser layer to shape the upstream spec rather than hand-editing the output. - Add @hey-api/openapi-ts dev dependency and a `gen:onshape-types` script. - openapi-ts.config.ts filters to just the configuration operations and patches the schema in place: narrows each `btType` to a literal, marks the fields the parser reads as required, trims unused branches, and rewrites allOf/discriminator polymorphism into explicit `oneOf` unions so the concrete subtypes survive orphan pruning. - Commit the generated output under src/backend/onshape-api/generated and re-export friendly aliases from onshape-api/types/configuration.ts. - Type getConfiguration/setConfiguration/decodeConfiguration and parseOnshapeConfiguration with the generated types; narrow on the `btType` discriminant in the parser. - Ignore the generated dir in eslint and allow linting openapi-ts.config.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Address review feedback on the configuration types work. - Rewrite openapi-ts.config.ts to be table-driven: a single `patch.input` with compact UNIONS/SCHEMAS tables and three small helpers, instead of a per-schema callback map. Adding an endpoint is now mostly adding rows. - Pin the top-level configuration response `btType` to its constant literal. - Keep `isCosmetic` required on the parameter schema, surface it on ParameterBase, and populate it in the parser (for upcoming use). - Rebuild the parser tests around a trimmed real configuration response (covering all parameter kinds, nested logical conditions, and both enum-option visibility shapes) instead of synthetic per-type fixtures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Pivot the Onshape typing approach: the @hey-api codegen now produces a committed *reference* slice only (nothing imports it), while the types we actually use are hand-authored under `onshape-api/types/*`. Hand-authoring lets the raw response types reuse our own enums (ConfigurationParameterType, Unit, QuantityType, OnshapeElementType, ...), carry comments, and form clean discriminated unions. - Rewrite types/configuration.ts as hand-authored types keyed on the shared enums; this removes the `as Unit` / `as QuantityType` / `as LogicalOp` casts in parse-configuration. - Add hand-authored types/{versions,documents,assemblies,part-studios}.ts covering the fields we read. `OnshapeElementType` moves to types/documents.ts (re-exported from the documents endpoint for existing importers). - Type the endpoints used by the load-document workflow and insert-and-fasten route: getVersions/getLatestVersion, getDocument/getContents, getAssembly, addElementToAssembly, addAssemblyFeature, getFeatures, addPartStudioFeature. - Type the consumers: load-document folder-tree helpers (now exported), the fasten parsers, and the document-thumbnail upload; drop the now-redundant casts. - Extend openapi-ts.config.ts to also emit versions/contents into the reference, adding a `keep` allowlist that prunes unread fields (and their dependency trees) before filtering. - Add pure-helper unit tests: getValidElements / getOrderedElementIds (load-document) and getFastenQuery (insert-and-fasten), and update the configuration fixtures to enum discriminants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Split the LoadDocumentWorkflow into a thin orchestrator plus a tested load-document-steps module, and make the per-element data flow explicit. - run() now delegates each step.do to an extracted function (fetchVersion, fetchContents, fetchExistingInsertables, matchElements, selectReloads, loadElementFasten, loadElementConfiguration, saveDocument), keeping the Workflow step names and the batch upsert/delete semantics. - Replace the unclear ValidElement + five elementId-keyed maps in save-to-db with an explicit pipeline: DocumentElement -> MatchedElement -> LoadedElement. A new early match-elements step resolves each element's insertable id (reused or a fresh UUID) and DB-matched fields up front, so saveDocument reads ids straight off each record — no re-query, no id juggling. - Gate fasten re-parsing on the element's supportsFasten flag (the user's enable intent) instead of fastenInfo IS NOT NULL. - matchElements takes an injected id generator so it's pure/testable; the memoized step supplies crypto.randomUUID for idempotent replays. - Add load-document-steps.test.ts: pure tests (getValidElements, getOrderedElementIds, matchElements, selectReloads, buildInsertable/GroupValues), mocked-Onshape tests (fetchVersion, fetchContents, loadElement*), and real-D1 tests (fetchExistingInsertables, saveDocument covering id reuse, preserved user columns, and stale deletes). Remove the superseded load-document.test.ts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Lift the group-level logic that was floating inside LoadDocumentWorkflow
into its callers, so the workflow becomes a pure "sync this document, at
this version, into this group" job.
- LoadDocumentParams drops documentId/libraryId/selectedGroupId, keeping
only { groupId, sessionId, version, forceReload? }. Since groupId already
identifies a row carrying documentId/libraryId, the workflow looks them
up itself via a new load-group step instead of having every caller
re-thread them through.
- run() no longer fetches the version, resolves/creates the group id, or
decides whether a reload is needed — all three move to routes/groups.ts.
- Add createOrderedGroup to library-data.ts: creates a group row and places
it in the library's sort order (after selectedGroupId, or at the end),
extracted from the workflow's old updateGroupOrder.
- /reload-groups now fetches each group's latest version itself and only
triggers the workflow for groups whose version changed (or all when
forceReload), skipping a group whose document can't be reached instead
of failing the whole batch.
- /group (add) fetches the version, generates the groupId, and calls
createOrderedGroup before triggering the workflow, so the new group
appears (and is ordered) immediately rather than only once the sync
finishes.
- Add library-data.test.ts (createOrderedGroup: append vs insert-after)
and routes/groups.test.ts coverage for the new reload-decision and
create+order+trigger behavior, spying on the LOAD_DOCUMENT_WORKFLOW
binding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Nothing imports the generated Onshape API reference anymore (confirmed by
grep) — the types actually used are hand-authored in
src/backend/onshape-api/types/*. So the config no longer needs to produce
precisely-typed, compileable output: drop the SCHEMAS map (btType pinning,
required-field marking, keep-allowlists) and DROP_DISCRIMINATORS entirely.
- UNIONS now auto-derives each union's members from the base schema's own
discriminator.mapping instead of a hand-typed member list, so new Onshape
subtypes show up without config changes. Discriminator subtypes are still
invisible unless expanded this way (hey-api doesn't turn allOf+discriminator
into a union on its own) — an early attempt to just replace the base schema
in place broke with a real TS2456 circular-reference error (confirmed via
tsc), since a subtype's allOf still extends the base it was replacing. Fixed
by keeping each union as a new sibling schema and only repointing the
handful of properties that reference the base polymorphically.
- Add a small OMIT map to keep the reference from ballooning on two newly
included operations (GET /documents/{did}, GET /assemblies/.../e/{eid}):
BTDocumentInfo's thumbnail/owner/workspace/user chain (and the same fields
re-declared on its allOf bases) pulled in ~40 unrelated schemas; trimmed
the same thumbnail/user-chain pattern from BTVersionInfo and
BTDocumentElementInfo for consistency.
- Explicitly did not add getFeatures/addAssemblyFeature/addPartStudioFeature
to operations.include — their response sits on Onshape's universal
FeatureScript feature-type discriminator (~359 schemas if expanded), not
reference material; assemblies.ts/part-studios.ts's feature-shaped fields
stay purely hand-authored.
- Regenerated output verified idempotent (two consecutive runs identical)
and typechecks clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Merge the 5 files under onshape-api/types/ into one
onshape-api/onshape-types.ts (with a // === <domain> === banner per endpoint,
matching the section-banner style already used internally), and rename +
gitignore the generated reference dump so the pairing and its "reference
only" status are both obvious from a file listing:
src/backend/onshape-api/
onshape-types.ts # hand-authored, committed
onshape-types.gen/ # generated by `npm run gen:onshape-types`, gitignored
No type names collided across the 5 source files (all already domain-prefixed),
so this is a straight concatenation. Update all 12 import statements across 10
consumer files (parse/* and onshape-api/endpoints/*) to the new path — two of
them (insert-and-fasten.ts, endpoints/part-studios.ts) had two separate
type-file imports that collapse into one. Update openapi-ts.config.ts's
`output` path and header comment, .gitignore, and eslint.config.js's
globalIgnores entry to match. The old committed, zero-importer generated/
directory is removed from git; the new onshape-types.gen/ never gets tracked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
…ce outside src/ - OnshapePartStudioFeature now carries name/suppressed (matching Onshape's BTMFeature-134), and parseFastenInfoFromPartStudio skips suppressed features when picking a mate connector, matching assembly-side behavior. - openapi-ts.config.ts derives union schema names mechanically from their discriminator base instead of hand-picking names that mirrored our hand-typed types. - The generated reference dump now lives at the repo root in onshape-api-reference/ and is committed rather than gitignored, but stays outside every tsconfig's scope so it never surfaces in editor autocomplete/auto-import.
…history
getDocument already returns recentVersion.{id,name} on the raw Onshape
response; we were ignoring it and instead calling getLatestVersion, which
downloads a document's entire version list just to take the last entry.
- OnshapeDocumentInfo now exposes recentVersion (un-omitted in the codegen
config, since it's a small self-contained BTBaseInfo, unlike the
thumbnail/owner chains that stay stripped).
- load-document-steps.ts replaces fetchVersion (list + take last) with
fetchVersionInfo(api, documentId, versionId), a single targeted
getVersion call for a known version id.
- groups.ts's POST /group route reuses the doc it already fetched for the
name, instead of a second full version-list fetch.
- groups.ts's POST /reload-groups loop now does a cheap getDocument-based
staleness check per group, only paying for the extra getVersion call on
groups that actually need reloading.
…Features in the reference - addElementToAssembly no longer branches on a useTransform option: the fire-and-forget instances endpoint was only ever taken when the caller discarded the result anyway, so every insert now goes through transformedinstances (with IDENTITY_TRANSFORM) and gets a real, consistently-shaped Onshape response. - Added getPartStudioFeatures to the codegen reference's operations.include, without adding its BTMFeature-134 base to UNION_BASES (which would pull in the ~350-schema FeatureScript discriminator fan-out). This surfaces the recursive subFeatures list and the plain mateConnectorFeature boolean needed for upcoming mate-connector sub-feature parsing, at the cost of only the feature list's own small schema tree.
parse/ mixed the load-document Workflow (orchestrator + steps) with unrelated, reusable parsing helpers (insert-and-fasten, parse-configuration, parse-vendors, build-checks) that aren't workflow-specific — insert-and-fasten is called directly from routes/insertables.ts, for instance. This made it unclear where a future workflow would live, and made the load-document / load-document-steps split feel like an arbitrary same-folder naming convention rather than a real boundary. load-document.ts and load-document-steps.ts now live in their own src/backend/load-document-workflow/ directory (steps.ts, dropping the redundant folder-scoped prefix), sitting as a sibling to routes/, onshape-api/, and parse/. A future workflow (e.g. thumbnail loading) follows the same <name>-workflow/ shape. parse/ now holds only the reusable parsing helpers it was always mostly made of.
Splits per-element loading out of the group workflow into its own class, LoadInsertable (load-insertable.ts): it owns everything about loading and persisting one insertable independently of the rest of the group - configuration, a discrete vendor-parsing step, thumbnails, insert-and-fasten, and finally its own D1 write. Each insertable now writes its own row rather than all elements landing in one batch at the end, so one element's exhausted retries no longer block the others or the group-level finalize (group row upsert, stale-insertable cleanup, search rebuild) - a stale microversionId is simply left in place, which the existing getInsertablesToReload check already treats as needing another reload. LoadDocumentWorkflow is renamed LoadGroupWorkflow (load-group-workflow/ load-group.ts), trimmed down to group-level orchestration now that the per-element logic has moved out. Its version-fetch step also simplifies to a single getDocument call, since the DB no longer needs the version's name/createdAt (see below) - just recentVersion.id. Also renames the groups/insertables schema's instanceId column to versionId, and drops insertables.versionName/versionCreatedAt (unused outside the backend, never displayed). conflictUpdateSet and the thumbnail-upload retry helper move to shared homes (db.ts, routes/thumbnails.ts) now that both the group and insertable loads need them.
…kflows conflictUpdateSet relied on Drizzle's onConflictDoUpdate to paper over whether a row already existed, but that made it easy to accidentally overwrite user-owned fields (isVisible, isOpenComposite, sortOrder) unless they were defensively excluded every time. Both workflows already know whether they're creating or updating, so they now branch explicitly: - LoadGroupParams is a discriminated union on isNew (true carries the sortOrder to create the group at; false doesn't need one, since an existing group's position is left alone). documentId/libraryId are now passed in directly instead of being looked up via a load-group step, which is gone. - LoadInsertableData gains isNew and hasConfiguration (the latter from a small extra query alongside the existing supportsFasten/microversionId lookup, since a not-new insertable can still be getting configured for the first time). LoadInsertable.save() inserts or updates each table based on those flags instead of upserting. This also lets POST /group drop its synchronous group-row insert: placeNewGroup (renamed from createOrderedGroup) now only computes the new group's sort position and renumbers its siblings, returning a plain number. The actual row is created by the workflow's own save-group step, same place it already writes on reload - the route just passes documentId/libraryId/sortOrder/isNew:true through to the workflow.
hasConfiguration required an extra precomputed query threaded through ExistingInsertable/MatchedElement/LoadInsertableData just so LoadInsertable.save() could pick insert vs update for the configurations row. That's cheap to determine right where it's needed instead: save() now just checks for an existing configurations row at the point of write. Group sort placement (placeNewGroup) moves from POST /group into the workflow's own save-group step, alongside the actual group insert. Doing this in the route meant sibling groups got renumbered to make room for a group that didn't exist yet for however long the async workflow took (or forever, if it failed). LoadGroupParams's isNew branch now carries selectedGroupId instead of a precomputed sortOrder, and saveGroup computes the position itself immediately before inserting - safe to retry as a unit, since the new group is never among the siblings placeNewGroup queries and renumbers.
The "Reload all documents" button sent a `reloadAll` query param, but
the backend's zValidator schema only reads `forceReload` — so the
field silently defaulted to false regardless of which button was
clicked, and unchanged groups (whose Onshape microversion hasn't
moved) were always skipped. This is why stale buildIssues (e.g.
NO_VENDORS) computed by older logic never got recomputed.
Also swap z.coerce.boolean() for z.stringbool(): Boolean("false") is
true in JS, so the old schema would have coerced an explicit
forceReload=false into true once the param name was fixed.
Add /reload-groups route test coverage (previously untested) and wire
src/shared/**/*.test.ts into the vitest "node" project, which was
silently excluded from both projects' include globs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
fetchContents now calls getDocument once and derives versionId from it, instead of a separate fetch-version step that fetched the document a second time just for its recentVersion. matchElements drops the bulk fetchExistingInsertables + in-memory Map match in favor of one select per element, scoped by groupId, libraryId, and elementId (the fields insertables are actually keyed on). A miss returns a MatchedInsertable with isNew: true and default fields; a hit returns the selected row's fields. Renamed MatchedElement -> MatchedInsertable to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
matchElements no longer needs libraryId in its select — groupId alone already uniquely scopes a group's insertables (unique index on elementId+groupId). It also drops insertableId/supportsFasten from its result entirely, since LoadInsertable now resolves those itself. LoadInsertableData shrinks to just what's needed to load an element from Onshape and know where to write it (no more insertableId/isNew/ supportsFasten passed through from the match phase). LoadInsertable adds its own resolve-insertable step: a select on groupId+elementId that finds the existing row (or assigns a fresh id for a new one), memoized like every other per-element step so a generated id stays stable across workflow replays. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ncv6XJzFCzAvtJt8LFMq3
Let editors flag an insertable for part-number search. When enabled, loading (or toggling) the insertable enumerates its enum/boolean configuration combinations, reads the Onshape part number Onshape reports for each, and stores a deduped part number -> configuration map. Part numbers become searchable, and selecting a part-number hit opens the insert menu pre-configured to the configuration that produces that part number. - Schema: insertables.searchPartNumbers (toggle) + insertables.defaultPartNumber (non-configurable parts) + configurations.partNumbers (the map); migration 0004. - Enumeration: shared enumerateConfigurations (enum + boolean only, quantity and string held at defaults), capped with a TOO_MANY_CONFIGURATIONS build issue. - Fetch/compute: getPartNumber endpoint + computePartNumbers with first-wins dedup keyed by part number. - Wired into the load workflow and a new toggle route that rebuilds the index. - Search index carries part numbers and the launch configuration; doSearch attaches the matched configuration to the hit. - Admin toggle menu item and build-status row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Handle Onshape's rate limiting (429 + Retry-After; X-Rate-Limit-Remaining on every response) so the heavy part-number fan-out no longer crashes into the limit or fails a reload. - Client (onshape-api.ts): throw OnshapeRateLimitError (carrying Retry-After seconds) on 429; expose lastRateLimitRemaining from X-Rate-Limit-Remaining. - Workflow: index part numbers in a post-pass that packs per-insertable jobs into parallel waves within the live remaining budget (job cost is known from enumerateConfigurations), seeded by a probe and updated from reported remaining. Each part-numbers-<id> step is durable with a Retry-After-aware retry (ONSHAPE_STEP_RETRIES) and a non-fatal backstop that flags PART_NUMBER_INDEX_INCOMPLETE, so a reload always completes even if slow. - computePartNumbers now reports the lowest observed remaining; add the pure nextWaveSize scheduler helper. - saveInsertable clears part-number fields; writePartNumbers persists the map, default part number, and part-number build issues in the post-pass. - Request path: central app.onError maps OnshapeRateLimitError to HTTP 429 with Retry-After; the inline toggle catches it and enables best-effort (next reload fills part numbers in). Assumes one reload workflow at a time and per-user limits (no cross-request coordination). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Split the part-number extraction rules out of the Onshape endpoint wrapper so they are pure and directly testable, matching the existing parse/ convention (parse-configuration, parse-vendors). - New parse/parse-part-number.ts: normalizePartNumber, parsePartStudioPartNumber, parseAssemblyPartNumber, with parse-part-number.test.ts covering them. - endpoints/parts.ts is now fetch-only (getParts, getAssemblyDefinition are exported); getPartNumber composes fetch + parse. - Reuse the canonical OnshapeAssemblyDefinition from onshape-types.ts instead of the local duplicate that shadowed it, adding rootAssembly.partNumber; add an OnshapePart type for the parts endpoint. Part numbers are normalized (trimmed, blank treated as unset) since they become search terms and PartNumberMap keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Restructure the config's comments without touching any code: shrink the 38-line header to what it needs to say (what this generates, that nothing imports it, how to run it), and move each explanation next to the constant it explains — the discriminator/union rationale onto UNION_BASES, the trimming rationale and the allOf re-declaration gotcha onto OMIT. Add a one-line comment on the union synthesis loop for symmetry with the repointing block below it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
placeNewGroup: it spliced the new id into the sibling array, filtered it back out to renumber, then used indexOf to recover where it landed. Compute the insertion index directly instead and renumber around it — siblings before the slot keep their index, those at or past it shift up by one. Behavior is unchanged (the existing tests pin it). This leaves the groupId parameter unused, so drop it: the function opens a slot, it doesn't need to know what fills it. Trim the doc comment to match. Part-number scheduling (loadPartNumbers, runPartNumberJob, selectPartNumberJobs, PartNumberJob) moves out of load-group.ts — ~140 lines, 30% of that file — and into load-part-numbers.ts, which already owned computePartNumbers and nextWaveSize. All part-number loading now lives in one module and load-group.ts is back to group orchestration. The moved code is otherwise unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Naming, per review: - tabs -> insertableTabs; fetchDocumentTabs -> fetchInsertableTabs; parseContents -> parseInsertableTabs; step contents-* -> insertable-tabs-* - toLoad -> insertablesToLoad; selectElementsToLoad -> selectInsertablesToLoad; step select-elements-* -> select-insertables-* - staleIds -> removedInsertableIds; findOrphanedRows -> findRemovedInsertables Structure: - Drop resolveGroupFields and InsertableGroupFields. libraryId is the only thing it fetched that callers didn't already have, so loadGroup takes it as a parameter; both workflows already have it. - Build the version path from the document (OnshapeDocumentInfo gains the `id` the API already returns) rather than from a stored documentId. - InsertableElement -> InsertableToLoad, carrying an explicit path plus libraryId/groupId, so loadInsertable and saveInsertable no longer take a separate group argument and no longer rebuild the path. - selectInsertablesToLoad branches explicitly on a stored row vs. a new tab instead of threading `??` defaults through one shared object literal. - Upload the document thumbnail after the insertables finish, awaited inline, instead of kicking it off early and awaiting it later. - Drop the rate-limit mention from the part-number comments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Part numbers were indexed by a group-level post-pass that re-queried every searchPartNumbers-enabled insertable regardless of whether it was reloaded, so an unchanged insertable burned up to 512 Onshape calls on every reload. That waste is largely why the rate-limit wave scheduler existed. Move the work into loadInsertable, so the existing reload decision (microversion comparison in selectInsertablesToLoad) applies for free: - load-part-numbers.ts: decompose into batchConfigurations + mergePartNumbers (pure) and fetchPartNumbers (I/O); loadPartNumbers now takes an insertable and runs one durable step per 20 configurations, each with ONSHAPE_STEP_RETRIES so a 429 is waited out per batch instead of re-fetching all 512. computePartNumbers stays as the unbatched path for the toggle route. Deletes the wave scheduler: runPartNumberJob, selectPartNumberJobs, PartNumberJob, nextWaveSize, PART_NUMBER_RATE_LIMIT_RESERVE, minRemaining. - load-insertable.ts: part numbers are computed before the save and written by it, so saveInsertable regains its partNumbers argument and ReloadedFields its defaultPartNumber. Deletes writePartNumbers/PartNumberWrite and the read-modify-write of buildIssues they needed. - The toggle route is now transactional: compute first, then write the flag with the data. No try/catch, so a failure leaves the flag off and reaches the client via app.onError (429 + Retry-After for rate limits) where the existing error toast warns the user. - Deletes PART_NUMBER_INDEX_INCOMPLETE (nothing degrades any more) and the now-unread lastRateLimitRemaining. 429/Retry-After handling stays. Adds the first tests for toggle-part-number-search, plus batchConfigurations and mergePartNumbers coverage and a test pinning the empty-columns invariant that getPartNumberMap relies on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
- create-app: return retryAfterSeconds in the 429 JSON body instead of a header,
so the client reads one shape.
- endpoints/parts.ts is now a plain API wrapper again: getPartNumber, which
composed a fetch with the parse layer, moves to load-part-numbers.ts.
- Part-number API returns the build issues it raises, with withPartNumberIssues
to merge them; the load and the toggle route now record issues identically,
and the route's inline let/if collapses to one call plus an indexPartNumbers
helper.
- defaultPartNumber is now always populated, configurable or not, so a capped
insertable still gets one. saveInsertable takes a single
InsertableConfiguration ({ parameters, partNumbers }) rather than two args.
- getOnshapeApiFromLoadContext -> getOnshapeApi; drop the local closure in
load-part-numbers.
- load-group: build the save batch as a conditional array instead of an
early-return; rewrite tab ordering to walk the folder tree and pick up tabs
(appending any the tree omits) rather than sort by a lookup with an Infinity
fallback.
- workflows: AddGroupParams moves above its class, forceReload folds into the
destructuring default, createShell -> createShellGroup, and a comment records
why these carry a sessionId (workflow params must be JSON-serializable, so an
OnshapeApi with its refresh callback cannot be passed).
- Tests spy on the endpoint wrapper instead of hand-rolling a fake client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
The word "configuration" meant three different things across the codebase. Disambiguate: - `Configuration` (a map of parameterId -> value) -> `ParameterValues` - `InsertableConfiguration` (the stored `configurations` row) -> `Configuration` - `ParameterObj` and its variants -> `ConfigurationParameter`, `EnumParameter`, `QuantityParameter`, `BooleanParameter`, `StringParameter` - `ParameterBase` -> `ConfigurationParameterBase` The parameter types took the names of the frontend components rendering them, so those get an `Input` suffix: `ParameterInput`, `EnumInput`, `BooleanInput`, `StringInput`, `QuantityInput`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Resolves two conflicts and the fallout of the configuration-type rename: - `create-app.ts`: keep both new imports (`HTTPException` from this branch, `getSessionCompanyId` from cert). - `worker-configuration.d.ts`: drop `ONSHAPE_API_VERSION` / `ONSHAPE_API_BASE_PATH`, which cert hardcoded as constants in `getBaseUrl()`. - `onshape-path.ts` and `insertable-card.tsx`: cert's new `configuration` fields hold parameter values, so they become `ParameterValues` under the rename. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
`0003_rename_version_id_drop_version_fields` (load-workflow rewrite) and `0004_add_part_number_search` are both unreleased, so fold them into a single `0003_version_id_rename_and_part_number_search`. Verified by applying all four migrations to a fresh local D1 and diffing the resulting columns against the Drizzle schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
…arch-indexing-731tsl
`isVisible` became a required field on `StoredInsertable` and `InsertableToLoad` in 675b55e, but the two fixture helpers build their base object literal before spreading `Partial<...>` overrides, so neither supplied it and tsc failed. Both default to `true`, matching the existing `is_visible` column default. That also keeps "preserves user-owned fields when reloading an existing row" meaningful: the row is inserted visible, the test hides it, and the reload has to leave it hidden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
…ng-731tsl' into claude/part-number-search-indexing-731tsl
**Part numbers.** `load/load-part-numbers.ts` is folded into `parse/parse-part-number.ts`, which now owns both loaders outright: `parsePartNumbers` for request handlers and `loadPartNumbers` for the workflow. They read the same four lines against different plumbing, sharing private `planPartNumberBatches` / `probePartNumber` / `fetchPartNumberBatch` / `toPartNumberResult`, so the injected `fetchDefault`/`fetchBatch` callbacks and the six internal exports (including `PART_NUMBER_BATCH_SIZE`) are gone. I/O in `parse/` matches the existing `parseFastenInfo`. Fixed along the way: `enumerateConfigurations([])` returns a single empty configuration, not none, so an insertable with nothing to vary was fetching its defaults twice and storing a map entry keyed to an empty configuration. **New MULTIPLE_PARTS build issue.** `parsePartStudioParts` reports whether the studio resolved to more than one part, OR'd across the default probe and every batch, so "ever has more than one part" means "at any configuration". Picking the first part carrying a number is only correct when there is one part. **Part-number issues are recomputed, not accumulated.** `clearBuildIssue` is variadic, and the toggle route clears `PART_NUMBER_ISSUE_TYPES` before adding the fresh ones. Previously `TOO_MANY_CONFIGURATIONS` stuck to a row forever: neither disabling the toggle nor re-indexing after trimming configurations could remove it. **Load types.** `GroupIdentity`, `InsertableToLoad`, and `ParseData` collapse into `GroupTarget` and `InsertableTarget`, both passed in rather than assembled from loose arguments — `loadGroup` drops from 6 parameters to 4, and its separate `versionId` goes away since `versionPath.instanceId` is the same value. `ReloadedFields` becomes `ParsedInsertable`, holding only what the load computed; `saveInsertable` owns the mapping to the write shape and takes 3 parameters. The user-owned flags leave `StoredInsertable` and `InsertableTarget` entirely: `loadInsertable` reads the two it needs in a step, and the save writes all three explicitly on insert, where a new insertable always wants `false`. **Schema.** `is_visible` goes back to `.default(true)` to match the DDL migration 0000 created. New insertables still start hidden — `saveInsertable` writes that explicitly, which is where the decision belongs. Verified against a fresh local D1 that every column default now agrees with `schema.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
Folded into the unreleased 0003 migration rather than a new one, and schema.ts goes back to .default(false). SQLite has no ALTER COLUMN, so this is a table recreate — but the obvious drizzle-style recreate silently destroys data on D1. `configurations` and `favorites` both reference `insertables(id)` ON DELETE CASCADE, and there is no way to switch that off: D1 runs migration statements in a transaction, where `PRAGMA foreign_keys=OFF` is silently ignored (it reads back as 1), and `defer_foreign_keys` only postpones violation reporting — cascade actions still fire. Verified both, and both wiped every configuration and every favorite. So the migration copies all three tables, drops the children before the parent so nothing references `insertables` when it goes, and renames `__new_insertables` last, letting SQLite rewrite the children's foreign keys back onto `insertables`. Verified by seeding pre-0003 data (a visible and a hidden insertable, a configuration, a favorite), then applying 0003: visibility, sort order, build issues, and both child rows survive; the default is false for new rows; the cascade and both unique indexes still work; no __new_ tables are left behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This combines the
claude/onshape-api-types-i44z11branch and the part-numbersearch work into one PR, since the second was built on the first.
certhasbeen merged in, and the two unreleased migrations are folded into one.
1. Typed Onshape API and hand-authored types
@hey-api/openapi-tsgenerates a reference of Onshape's OpenAPI spec intoonshape-api-reference/(committed, lint-ignored, regenerated withnpm run gen:onshape-types). It is a reference only — nothing imports it.onshape-api/onshape-types.ts, because the generated ones are far too looseto be useful (almost everything optional). The reference exists so those
hand-authored types can be checked against what Onshape documents.
onshape-api/endpoints/now has a typed return.Endpoints stay thin shells around the HTTP call — parsing and business logic
live in
parse/.configuration enums were split into
shared/configuration-enums.ts.2. Load workflow rewrite
parse/load-document.ts(621 lines, one monolithic workflow) is replaced bysrc/backend/load/:workflows.tsLoadLibraryWorkflowandAddGroupWorkflowentry pointsload-group.tsload-insertable.tsload-part-numbers.tsload-utils.tsLoadContext, shared step helpers, retry policyOther changes in this half:
library.
(
getLatestVersionId) and threaded intoloadGroup, rather than beingderived from the document info.
versionName/versionCreatedAtare dropped, andinstanceIdis renamed toversionIdongroupsandinsertables.hadn't changed.
hasConfigurationis gone; group placement moved into the workflow.3. Part-number search
Insertables get a per-insertable Search part numbers toggle. When enabled,
the load enumerates the insertable's configuration combinations, asks Onshape
for the part number of each, and stores a part number → parameter values map so
a search hit can launch the configuration that produces it.
parameters ride on their Onshape defaults, since they're unbounded.
configurations resolve to the same part (parameters that don't affect
geometry), and keying this way collapses them so search never surfaces
duplicates of one part.
configurationsrow next toparameters;a non-configurable insertable stores its single number in
insertables.default_part_number.TOO_MANY_CONFIGURATIONSbuild issue and isn't indexed.re-fetches only that batch rather than up to 512 calls.
OnshapeRateLimitErrorcarryingRetry-After; steps retry with that delay,and the request path maps it to a 429 with
retryAfterSecondsin the JSONbody.
loadInsertable, so the existingmicroversion check gates it — an unchanged insertable is skipped on reload
instead of re-issuing up to 512 calls.
only then writes the flag alongside them, so a failure leaves the flag off
and warns the user rather than persisting a half-built index.
Search-side,
buildSearchDbindexes the part numbers as a field and carries theconfiguration map, so a matched number resolves to the configuration to open.
4. Naming
The word "configuration" meant three different things, so the types were
disambiguated:
Configuration(parameterId → value)ParameterValuesInsertableConfiguration(the stored row)ConfigurationParameterObjand variantsConfigurationParameter,EnumParameter, …The parameter types took the names of the components that render them, so those
gained an
Inputsuffix (EnumInput,QuantityInput, …).5. Migrations
0003_rename_version_id_drop_version_fieldsand0004_add_part_number_searchare both unreleased, so they're folded into a single
0003_version_id_rename_and_part_number_search.Verification
npm run tsc,npm run lint, andnpm test(161 tests, 21 files) all pass.All four migrations were applied to a fresh local D1 via
npm run devand theresulting columns diffed against the Drizzle schema.
Not yet exercised manually against live Onshape — enabling the toggle on a real
configurable insertable and confirming a reload skips unchanged insertables is
still worth doing before merge.
Noted, not fixed here
deploy:certcan't apply migrations (pre-existing oncert).da2e21cfixed
npm run devto pass theDBbinding, butdeploy:certstill passesdb, which matches neither the binding nor the cert database's name(
frc-design-app-cert-db). Wrangler then can't resolvemigrations_dirandfails with "No migrations present at ./migrations", so the migration in this
PR wouldn't apply on a cert deploy.
deploy:productionis fine —db-productionis that database's actual name.isVisible's schema default diverges from the column default.675b55echanged
schema.tsto.default(false), but migration0000createdis_visiblewithDEFAULT trueand nothing reconciles them. No runtimeeffect today, since
saveInsertablealways writes the value explicitly, butdrizzle-kit generatewill want a migration for it.Open question
loadGroupthrows when an insertable fails to load, failing the group. A buildissue on that insertable might be better — it would let the rest of the group
finish. Left as a throw for now.
🤖 Generated with Claude Code
https://claude.ai/code/session_01XT52xco8DfQnh1iMGff1gF