Skip to content

API Proxy Update to Operations 2.21

Analysis of the deployed Operations OpenAPI spec at https://dev.alpha002.io.arda.cards/v1/item/docs/openApi.json compared against the current @arda-cards/api-proxy ItemProxy implementation. Identifies gaps and proposes changes.

  • OpenAPI spec: Operations 2.21, deployed to dev environment
  • api-proxy: src/reference/item/proxy.ts (current main branch)

The backend has deployed a new image upload credential endpoint.

POST /v1/item/image-upload

This is a module-scoped endpoint (under /v1/item/), not entity-scoped. There is no itemEId path parameter. This differs from the system design specification which assumed POST /v1/item/item/<itemEId>/image-upload-url (TD-13). The goal.md and BFF specification documents should be updated to reflect the actual deployed path.

{
contentType: string; // required — e.g. "image/jpeg"
contentLength: number; // required — int64, file size in bytes
}
{
uploadUrl: string; // required — presigned S3 POST URL
formFields: Record<string, string>; // required — form fields for multipart POST
objectKey: string; // required — S3 object key
cdnUrl: string; // required — CDN URL for the uploaded image
}
HeaderRequiredDescription
X-AuthorYesAuthor of the call
X-Tenant-IdYesTenant identifier
X-Request-IDNoServer generates one if not provided

Standard ErrorResponse for all non-200 status codes (400, 401, 403, 413, etc.).


CapabilityEndpointProxy Method
Create itemPOST /v1/item/itemcreate()
Get itemGET /v1/item/item/{id}get()
Get by record IDGET /v1/item/item/rid/{rid}getByRecordId()
Update itemPUT /v1/item/item/{id}update()
Delete itemDELETE /v1/item/item/{id}delete()
Query itemsPOST /v1/item/item/queryquery()
Query historyPOST /v1/item/item/{id}/historyqueryHistory()
Get draftGET /v1/item/item/{id}/draftgetDraft()
Update draftPUT /v1/item/item/{id}/draftupdateDraft()
List suppliesGET /v1/item/item/{id}/supplygetSupplies()
Create supplyPOST /v1/item/item/{id}/supplycreateSupply()
Update supplyPUT /v1/item/item/{id}/supply/{sid}updateSupply()
Delete supplyDELETE /v1/item/item/{id}/supply/{sid}deleteSupply()
Lookup suppliersGET /v1/item/lookup-supplierslookupSuppliers()
Lookup unitsGET /v1/item/lookup-unitslookupUnits()
Print labelsPOST /v1/item/item/print-labelprintLabels()
Print breadcrumbsPOST /v1/item/item/print-breadcrumbprintBreadcrumbs()
CSV upload URLPOST /v1/item/upload-job/upload-urlcreateUploadUrl()
CSV upload statusGET /v1/item/upload-job/{jobId}getUploadJobStatus()

Missing — Image Upload (new in Operations 2.21)

Section titled “Missing — Image Upload (new in Operations 2.21)”
EndpointMethodPurposePriority
/v1/item/image-uploadPOSTPresigned S3 POST credentials + CDN URLMust-do
EndpointMethodPurposePriority
/v1/item/item/{id}/draftDELETEDiscard draftShould-do

The api-proxy has getDraft() and updateDraft() but is missing deleteDraft().

All follow the same pattern as lookupSuppliers/lookupUnits: GET with a name query parameter, returning string[].

EndpointMethodPurposePriority
/v1/item/lookup-departmentsGETFuzzy match department namesShould-do
/v1/item/lookup-facilitiesGETFuzzy match facility namesShould-do
/v1/item/lookup-itemsGETFuzzy match item namesShould-do
/v1/item/lookup-locationsGETFuzzy match location namesShould-do
/v1/item/lookup-sublocationsGETFuzzy match sublocation namesShould-do
/v1/item/lookup-subtypesGETFuzzy match subtype namesShould-do
/v1/item/lookup-typesGETFuzzy match type namesShould-do
/v1/item/lookup-usecasesGETFuzzy match use case namesShould-do

Note: lookup-departments and lookup-facilities are new lookups not previously available. The others (lookup-items, lookup-locations, etc.) may have been available before but were never added to the api-proxy.

EndpointMethodPurposePriority
/v1/item/item/bulkPOSTBulk create itemsMust-do
/v1/item/item/bulkPUTBulk update itemsMust-do
/v1/item/item/query/{page}GETCursor-based paginated queryMust-do
/v1/item/item/{id}/history/{page}GETCursor-based paginated historyMust-do

No current consumer in arda-frontend-app, but included for full API coverage.

The imageUrl field is already string | null in both the deployed OpenAPI spec and the existing api-proxy ItemInput type. No type changes needed for image URL persistence.


Types (src/reference/item/types.ts):

/** Request for presigned S3 POST credentials to upload an item image. */
export interface ImageUploadRequest {
contentType: string;
contentLength: number;
}
/** Presigned S3 POST credentials and CDN URL for an uploaded image. */
export interface ImageUploadResponse {
uploadUrl: string;
formFields: Record<string, string>;
objectKey: string;
cdnUrl: string;
}

Proxy method (src/reference/item/proxy.ts):

// -------------------------------------------------------------------------
// Image upload operations
// -------------------------------------------------------------------------
createImageUploadUrl(request: ImageUploadRequest): Promise<ImageUploadResponse> {
return this.client.request("POST", this.client.buildUrl("/image-upload"), request);
}

Tests (tests/reference/item/proxy.test.ts):

  • Happy path: correct URL construction, request body passed, response parsed
  • Verify path is /v1/item/image-upload (module-scoped, not entity-scoped)
  • Error propagation (non-200 status)

Proxy method (src/reference/item/proxy.ts):

deleteDraft(entityId: string): Promise<ItemRecord> {
return this.client.request("DELETE", this.client.buildUrl(`/item/${entityId}/draft`));
}

Tests: Happy path, error propagation.

All follow the existing lookupSuppliers pattern:

lookupDepartments(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-departments", { name: query });
}
lookupFacilities(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-facilities", { name: query });
}
lookupItems(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-items", { name: query });
}
lookupLocations(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-locations", { name: query });
}
lookupSublocations(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-sublocations", { name: query });
}
lookupSubtypes(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-subtypes", { name: query });
}
lookupTypes(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-types", { name: query });
}
lookupUsecases(query: string): Promise<string[]> {
return this.client.lookup<string[]>("lookup-usecases", { name: query });
}

Tests: One test per method following the existing lookup test pattern.

Types (src/reference/item/types.ts):

/** Entry in a bulk update request. */
export interface BulkUpdateEntry {
eId: string;
payload: ItemInput;
}
/** Request body for bulk update operations. */
export interface BulkUpdateRequest {
updates: BulkUpdateEntry[];
}

Proxy methods (src/reference/item/proxy.ts):

bulkCreate(inputs: ItemInput[]): Promise<ItemRecord[]> {
return this.client.request("POST", this.client.buildUrl("/item/bulk"), inputs);
}
bulkUpdate(request: BulkUpdateRequest): Promise<ItemRecord[]> {
return this.client.request("PUT", this.client.buildUrl("/item/bulk"), request);
}

Tests: Verify URL /v1/item/item/bulk, correct HTTP method, request body, array response.

Proxy methods (src/reference/item/proxy.ts):

getQueryPage(pageId: string): Promise<ItemPage> {
return this.client.request("GET", this.client.buildUrl(`/item/query/${pageId}`));
}
getHistoryPage(entityId: string, pageId: string): Promise<ItemPage> {
return this.client.request("GET", this.client.buildUrl(`/item/${entityId}/history/${pageId}`));
}

Tests: Verify URL construction with page ID tokens, GET method, PageResult response.

Add new types to src/reference/item/index.ts and src/reference/index.ts barrel exports:

export type {
BulkUpdateEntry,
BulkUpdateRequest,
ImageUploadRequest,
ImageUploadResponse,
} from "./types.js";

All changes described above have been implemented and verified:

  • Typecheck: passes
  • Lint: passes
  • Tests: 225 total (39 for ItemProxy — 14 new tests added)
  • Build: passes

New methods added to ItemProxy:

  • createImageUploadUrl(request) — image upload credentials
  • deleteDraft(entityId) — discard draft
  • lookupDepartments(query) — 8 new lookup methods
  • lookupFacilities(query)
  • lookupItems(query)
  • lookupLocations(query)
  • lookupSublocations(query)
  • lookupSubtypes(query)
  • lookupTypes(query)
  • lookupUsecases(query)
  • bulkCreate(inputs) — bulk create
  • bulkUpdate(request) — bulk update
  • getQueryPage(pageId) — cursor pagination for query
  • getHistoryPage(entityId, pageId) — cursor pagination for history

The deployed endpoint path /v1/item/image-upload differs from the system design assumption of /v1/item/item/<itemEId>/image-upload-url (TD-13). The following documents have been updated:

  • goal.md — corrected: backend path, BFF route, deliverables, scope, constraints, and success criteria now reference /v1/item/image-upload (backend) and /api/image-upload (BFF)
  • upload-component-backend-analysis.md — corrected: API functions layer, connection point table, PlantUML diagram, and hook examples now use the module-scoped path

The following documents in the system design section still reference the original entity-scoped path and should be reviewed in a future update:

  • BFF specification — proxy route assumed at POST /api/item/<itemEId>/image-upload-url
  • SPA specification — outbound interface table

The BFF route path (/api/...) is a design choice for arda-frontend-app and does not need to mirror the backend path exactly. However, the BFF route handler must call the correct backend path (/v1/item/image-upload).


Copyright: (c) Arda Systems 2025-2026, All rights reserved