Design Session 03: Item Module API Changes for Image Upload
Design Intent and additional requirements
Section titled “Design Intent and additional requirements”The System must support Item Images from either external URLs or files uploaded to S3.
Currently the System only supports external URLs in the Item entity. The improvement requires:
- Item Updates can provide an external URL or a
keythat resolves to an S3 Object, or both. - An Item, when queries or read does not show the difference between an external URL and an S3 Object but simply provides the URL that can be used to retrieve the image directly. In the case of S3 objects, the URL is resolved natively by the S3 service or a CDN attached to it.
- CSV imports should support both external URLs and S3 keys, failing if the S3 keys do not resolve to valid pre-uploaded objects.
- Updates to existing items and CSV uploads should only require the
uuidpart of the object key to be provided. The system needs to resolve the full key and synthesize the image URL based on the provided uuid, the tenant id (fromApplicationContext) and by trying to resolve the object from S3 trying the allowed extensions.
Guidance
Section titled “Guidance”- The Core machinery of the ItemDataAuthority (an
EditableDataAuthority) should be kept as closely as possible to the vainilla implementation of the base class. - The
ItemInputshould have an additionalimageKeyfield of typeUUIDthat is not required. - Translation from ItemInput to Item should take care of the resolution of the effective image URL:
- If no
imageKeyorimageUrlis provided, the resultingItemshould haveimageUrlset tonull. - If only
imageKeyis provided, the resultingItemshould haveimageUrlset to the resolved URL if it exists or result in an error otherwise. - If only
imageUrlis provided, the resultingItemshould haveimageUrlset to the provided URL. - If both
imageKeyandimageUrlare provided, the resultingItemshould haveimageUrlset to the resolvedimageKeyURL. IfimageKeydoes not resolve to a valid S3 object, the operation fails with an error — no fallback toimageUrl. (Clarified per CQ-7.)
- If no
- The Translation from previous point, could be done as a purely functional method in the
ItemService(no updates performed) or as an additional update method in the service (with side effect of updating theItemtransactionally). In any case, isolating the resolution logic in a separate method is required. TBD whether an additional update method is required or the sequence of translation and update is done in the Endpoint Logic plus the CSV importer.
Initial Open Questions for This Session
Section titled “Initial Open Questions for This Session”OQ-1: imageUrl vs imageKey — field transition strategy.
The current Item entity has imageUrl: URL? storing a full URL (set via CSV
import or direct API input). The new design introduces imageKey (an S3 object
key). What is the intended relationship?
- Replace: Rename
imageUrl→imageKeyat all layers? Breaks backward compatibility for clients sending full URLs. - Coexist: Keep
imageUrlfor external URLs, addimageKeyfor managed S3 assets? Read path prefersimageKeyoverimageUrlwhen both are set. - Absorb: Keep
imageUrlas the API-facing field but accept both full URLs and S3 keys, resolving internally?
Affects persistence layer, API model, CSV proto, and the read path.
JMP See guidance above.
OQ-2: Read path URL resolution. When the client reads an Item entity, what does it get back for the image?
- A raw S3 key that the SPA resolves using a known base URL?
- A fully resolved URL the SPA can use directly in
<img src="...">? - Both (key for updates, resolved URL for display)?
Interacts with DQ-006’s three-phase CDN rollout — if the entity stores a key rather than a URL, the base URL can change across phases without data migration.
JMP See guidance above. Fully resolved URL.
OQ-3: Upload endpoint path convention.
DS-02 specifies POST /v1/item/<itemId>/image/upload-url, but the current API
uses a doubled module/entity pattern: /v1/item/item/{entity-id}. Should the
upload endpoint follow this convention:
/v1/item/item/{entity-id}/image/upload-url?
JMP Keep the double module/entity pattern. Need to update the decision log.
OQ-4: Draft items. The Item Module has draft operations (create/update/promote). Should the image upload workflow work with draft items? Can a user upload an image to a draft that hasn’t been promoted yet?
JMP Yes, the image upload workflow is decoupled from item editing. Sequence diagrams should be enhanced to show how upload interacts with draft items.
JMP Guidance: In the current diagrams, the Item update is shown as a single step. In reality, before the scenario starts, the Item may have been put in a draft state. The update is done to the draft and then the draft is published. The image upload is done at any time before the publish step that validates the inputs, including the existence of the image object based on the resolved URL.
OQ-5: Bulk operations and imageKey.
Bulk add/update endpoints exist. While bulk image upload is out of scope, should
bulk entity operations accept imageKey in their payloads (with the same HEAD
validation)? Or is imageKey restricted to single-entity operations initially?
JMP See guidance above.
OQ-6: Scope of DS-03 deliverables. What should this session produce? Candidates:
- Detailed Ktor route specs for the new upload endpoint
ItemInput/Itemmodel changes (field additions, type changes)- Persistence migration plan (
image_urlcolumn changes or newimage_keycolumn) - Integration design showing how the HEAD check fits into
ItemService.update() - Read-path URL resolution design
- All of the above as a complete “Item Module changes specification”
JMP
- Updated Sequence Diagrams with details of editing with draft lifecycle.
- Sequence Diagrams for the
CreateItemscenario. - Sequence Diagrams for the CSV import scenario.
- Ktor Route Specs, preferably using the Endpoint DSL in
common-module/.../ServiceEndpointDsl.ktor pseudo-code close to it. - Detailed scenarios for Object Key validation during Item Update service calls (both from API and CSV import).
- Detailed Requirements for the S3 Access abstraction, ideally expressed as a documented Kotlin Interface.
OQ-7: Object eviction. The decision log lists “Object Eviction Requirements and scenarios” as a pending open point. Should DS-03 address this, or is it deferred to a separate session?
JMP Deferred to a separate session.
Prior Decisions (Context)
Section titled “Prior Decisions (Context)”Decisions from Design Session 01 and Design Session 02 that constrain this session:
- DQ-001 — Key format:
${tenantId}/${owning-module}/${entity-type}/${property-name}/${asset-uuid}.${extension} - DQ-003 — Shared library in
common-module(not Lambda) - DQ-005 — Option A with mandatory HEAD check: three-step workflow (request presigned POST, upload to S3, entity update with HEAD validation). Entity update returns 422 if S3 object not found.
- DQ-007 — New
S3BucketAccessabstraction incommon-module
From Design Session 02 — Requirements relevant to this session:
- E-1: New endpoint
POST /v1/item/<itemId>/image/upload-url - E-2: Existing
PUT /v1/item/<itemId>extended withimageKey+ HEAD validation - M-1 through M-8: Pre-checks, HEAD validation, persistence, configuration
Current Item Module API
Section titled “Current Item Module API”Reference: OpenAPI spec
| Implementation: /operations/src/main/kotlin/cards/arda/operations/reference/item/
Endpoints (Complete Inventory)
Section titled “Endpoints (Complete Inventory)”Standard CRUD
Section titled “Standard CRUD”| Method | Path | Operation | Notes |
|---|---|---|---|
POST | /v1/item/item | Create item | Body: ItemInput. Query: effective-as-of?, mutation-mode? |
GET | /v1/item/item/{entity-id} | Get item by ID | Query: as-of? |
PUT | /v1/item/item/{entity-id} | Update item | Body: ItemInput. Query: effective-as-of?, mutation-mode? |
DELETE | /v1/item/item/{entity-id} | Delete item | Cascades to child ItemSupply entities |
GET | /v1/item/item | List/page items | Query: as-of?, pagination |
GET | /v1/item/item/rid/{record-id} | Get by record ID | Bitemporal record lookup |
Bulk Operations
Section titled “Bulk Operations”| Method | Path | Operation |
|---|---|---|
POST | /v1/item/item/bulk | Bulk add |
PUT | /v1/item/item/bulk | Bulk update |
GET | /v1/item/item/bulk | Bulk get |
Draft Operations
Section titled “Draft Operations”| Method | Path | Operation |
|---|---|---|
POST | /v1/item/item/draft | Create draft |
GET | /v1/item/item/{eId}/draft | Get draft |
PUT | /v1/item/item/{eId}/draft | Update draft |
DELETE | /v1/item/item/{eId}/draft | Delete draft |
POST | /v1/item/item/{eId}/draft/promote | Promote draft |
Supply Sub-Resource (/v1/item/item/{parent-item-id}/supply)
Section titled “Supply Sub-Resource (/v1/item/item/{parent-item-id}/supply)”| Method | Path | Operation | Notes |
|---|---|---|---|
POST | .../supply | Create supply | Body: ItemSupplyInput |
PUT | .../supply/{item-supply-id} | Update supply | Body: ItemSupplyInput |
DELETE | .../supply/{item-supply-id} | Delete supply | |
GET | .../supply | List supplies |
Lookup (Unauthenticated Typeahead)
Section titled “Lookup (Unauthenticated Typeahead)”| Method | Path | Returns |
|---|---|---|
GET | /v1/item/item/lookup-items | [{name, eId}] |
GET | /v1/item/item/lookup-suppliers | [string] |
GET | /v1/item/item/lookup-types | [string] |
GET | /v1/item/item/lookup-subtypes | [string] |
GET | /v1/item/item/lookup-usecases | [string] |
GET | /v1/item/item/lookup-units | [string] |
GET | /v1/item/item/lookup-facilities | [string] |
GET | /v1/item/item/lookup-departments | [string] |
GET | /v1/item/item/lookup-locations | [string] |
GET | /v1/item/item/lookup-sublocations | [string] |
All take as-of (required), name (search term), limit? (default 10).
CSV Upload
Section titled “CSV Upload”| Method | Path | Operation |
|---|---|---|
POST | /v1/item/upload-job/upload-url | Get presigned PUT URL |
POST | /v1/item/upload-job/{job-id} | Trigger processing |
GET | /v1/item/upload-job/{job-id} | Poll job status |
Printing
Section titled “Printing”| Method | Path | Operation |
|---|---|---|
POST | /v1/item/item/{eId}/print-label | Print labels |
POST | /v1/item/item/{eId}/print-breadcrumb | Print breadcrumbs |
Common Headers
Section titled “Common Headers”| Header | Required | Description |
|---|---|---|
X-Tenant-Id | Yes | Tenant scoping (UUID) |
X-Author | Yes | Author identifier (string) |
X-Request-Id | No | Request correlation ID |
Data Model: ItemInput (Request Body)
Section titled “Data Model: ItemInput (Request Body)”| Field | Type | Nullable | Notes |
|---|---|---|---|
name | String | No | Required, natural identifier |
description | String | Yes | |
imageUrl | String (parsed to URL) | Yes | Current image field — full URL |
classification | {type, subType?} | Yes | ItemClassification.Value |
useCase | String | Yes | |
glCode | String | Yes | |
locator | {facility, department?, location?, subLocation?} | Yes | PhysicalLocator.Value |
internalSKU | String | Yes | |
minQuantity | {amount, unit} | Yes | Quantity.Value |
notes | String | Yes | |
cardNotesDefault | String | Yes | |
taxable | Boolean | Yes | |
primarySupply | ItemSupplyReference.Value | Yes | Embedded supply reference |
secondarySupply | ItemSupplyReference.Value | Yes | Embedded supply reference |
defaultSupply | String | Yes | Supplier name |
defaultSupplyEId | UUID | Yes | Soft FK to supply entity |
cardSize | CardSize enum | Yes | SMALL, MEDIUM, LARGE, X_LARGE |
labelSize | LabelSize enum | Yes | Same values |
breadcrumbSize | BreadcrumbSize enum | Yes | Same values |
itemColor | ItemColor enum | Yes | BLUE, GREEN, YELLOW, ORANGE, RED, PINK, PURPLE, GRAY |
Data Model: Item (Domain Entity)
Section titled “Data Model: Item (Domain Entity)”Same fields as ItemInput plus eId: UUID. The domain entity stores imageUrl
as a URL?. This is the field that the new imageKey mechanism will
replace/augment.
Persistence: ITEM Table
Section titled “Persistence: ITEM Table”The imageUrl column is stored as image_url (URL type, nullable) in the
ITEM table via ScopedTable. See
/operations/src/main/kotlin/cards/arda/operations/reference/item/persistence/ItemPersistence.kt.
Key Implementation Notes
Section titled “Key Implementation Notes”-
mutation-modequery parameter controls supply reference behavior on create/update:LAX— trust input, no cross-entity checks (CSV import)STRICT— verify references exist and matchPROPAGATE— cascade: auto-create/update ItemSupply + BusinessAffiliate (default for API)
-
ItemEndpoint.ImplextendsEditableDataAuthorityEndpoint, which provides the standard CRUD + draft + bulk routes. Custom routes (upload, supply, lookup, printing) are added inbuildRoot(). -
CSV upload is wired in
Module.ktviaCsvUploadRoutes, usingCsvS3BucketDirectAccessfor presigned PUT URLs andJobServicefor tracking. The upload routes are outside theItemEndpointclass, added directly to the Ktor module. -
ItemService.update()enriches the update payload, runsPersistencePreparer.forUpdate, and propagates supply changes. The HEAD check forimageKeywill need to integrate into this flow. -
Module wiring (
Module.ktline 126): the CSV bucket is resolved fromextras["uploadBucketArn"]. The newhttp-assetsbucket will need a similar configuration entry.
Discussion
Section titled “Discussion”Additional Materials:
- Explore the Current SPA logic for Item Creation and Editing in the
arda-frontend-apprepository.
Step 0: Current SPA Interaction
Section titled “Step 0: Current SPA Interaction”JMP: Create PlantUML sequence diagram showing SPA, BFF and Backend as participants for:
- Item Creation Scenario
- Item Draft Creation and Update Scenario
- Item Draft Publishing.
CLD-AI: Based on code exploration of arda-frontend-app, operations, and
api-test, here are the three current interaction patterns. Internal service
details (supply enrichment, persistence preparation) are abstracted away to
focus on the workflow structure relevant to the upload design.
Key implementation facts discovered:
- Draft is a prerequisite for update.
EditableDataAuthorityService.updater()checksdraftStore.current()and fails with"Cannot update {eId} without a draft"if no draft exists. - GET /draft is get-or-create. It creates a draft from the current published state if none exists, or returns the existing draft.
- Publish = standard PUT. There is no separate promote endpoint. The PUT on
/v1/item/item/{eId}checks for a draft, closes it, and updates the universe. The SPA treats this as “Save.” - Draft update body differs from item update. PUT on
/draftuses a wrapped body{ entityId, value: {...}, metadata, author }. PUT on the item uses flatItemInput.
Current: Item Creation
Section titled “Current: Item Creation”Current: Draft Creation and Update
Section titled “Current: Draft Creation and Update”Current: Item Publish (Draft Promotion via Update)
Section titled “Current: Item Publish (Draft Promotion via Update)”Key observations for the upload design:
-
ItemInput.toItem(eId)is the integration point. Currently it convertsimageUrl: String?toURL?viaURI(it).toURL(). The newimageKey: UUID?resolution logic fits naturally into this translation step — or into a separate resolver method called from the same place. -
Draft stores working state without validation. This is exactly the semantics needed for
imageKey: the user can save a draft with animageKeyUUID, and validation is deferred to publish time. -
The SPA controls sequencing. Upload, draft save, and publish are all separate SPA-initiated calls. The SPA can upload an image, store the
imageKeylocally, include it in draft saves, and send it in the final publish PUT.
Step 1: Item Creation with Image Upload
Section titled “Step 1: Item Creation with Image Upload”JMP: Propose a PlantUML sequence diagram similar to those in design-session-02.md for Item Creation.
- Image Upload must precede the API creation call
- API creation call verifies object exists in S3.
General Idea: SPA collects all information from the user, if the user wants to
upload an image as part of the item, the SPA will upload it to the S3 bucket and
set the imageKey field in the ItemInput payload. When the SPA calls the API,
the image object should already exist in S3.
CLD-AI:
Design notes:
-
Upload endpoint has no entity-id. Unlike DS-02’s original spec (
POST /v1/item/item/{itemId}/image/upload-url), this endpoint does not require an entity-id because during creation the item doesn’t exist yet. The S3 key doesn’t include entity-id either (per DQ-001). See Actions. -
Response includes
imageKey: UUID. The upload endpoint returns the UUID separately so the client doesn’t need to parse the fullobjectKey. The client usesimageKeyin the subsequent creation call. -
Extension probing. The backend probes up to 3 extensions (jpg, png, webp) via HEAD calls. This adds 1—3 S3 API calls per resolution. For single-item operations this is negligible latency (~5ms each).
-
Resolution at create time. The
resolveImageKeymethod is called duringItemInputtoItemtranslation, before the item is persisted. If resolution fails, the entire create operation fails with 422. -
Persisted value is
imageUrl, notimageKey. TheItementity stores the resolvedimageUrl: URL(e.g.,{base-url}/{tenantId}/operations/item/image/{uuid}.png). TheimageKey: UUIDis transient — it exists only inItemInput. No persistence schema change is needed.
Step 2: Detailed Scenarios to Integrate Draft Lifecycle with Image Uploads
Section titled “Step 2: Detailed Scenarios to Integrate Draft Lifecycle with Image Uploads”JMP: Propose a PlantUML sequence diagram similar to those in design-session-02.md for updates with Draft lifecycle.
Note General Idea in previous step, similar logic for the SPA should apply here.
Conditions:
- Draft Creation (
GET /v1/item/item/{eId}/draft) is independent of Uploads. - Draft Updates do not verify imageKey.
- Draft Publish (Item Update) verifies imageKey.
CLD-AI:
Design notes:
-
Four distinct phases. Draft lifecycle and image upload are decoupled: phases 1 and 3 are standard draft operations; phase 2 (upload) can happen at any time before phase 4 (publish).
-
Draft stores
imageKeywithout validation. When the user saves a draft (phase 3),imageKey: UUIDis stored in the draft’svalueobject alongside other item fields. The backend does not resolve or validate it — the draft is a working scratch pad. -
Validation deferred to publish. The HEAD check and URL resolution happen only in phase 4, during the
PUT /v1/item/item/{eId}processing. This ensures that at the moment the item is published, the referenced S3 object is confirmed to exist. -
Resuming a saved draft. If the user saves a draft with
imageKeyand returns later, the SPA retrieves the draft (which includesimageKeyinvalue) and can display the image using a locally-constructed preview URL. On publish, the backend re-validates via HEAD. -
Update with only
imageUrl(noimageKey). If the user provides an external URL instead of uploading, the flow skips phase 2 entirely. The PUT payload includesimageUrlwithoutimageKey, and the standard translation applies the URL directly. -
resolveImageKeyis the same method used in Step 1 (item creation) and here (item update/publish). It is called duringItemInputtoItemtranslation, which happens in both the create and update code paths.
Step 3: CSV Item Uploads
Section titled “Step 3: CSV Item Uploads”JMP: Identify questions to resolve before a design (scenario) can be proposed.
Exploration only: Ask Questions to frame the problem.
CLD-AI: The following questions need to be resolved before a CSV import scenario can be diagrammed:
CSV-Q1: Column representation. The current CSV proto has
string image_url. How should S3 keys be specified in CSV rows?
- (a) Add a new
string image_keycolumn that accepts UUIDs. Rows can populate eitherimage_urlorimage_keybut not both (same semantics as the API). - (b) Reuse the existing
image_urlcolumn. The backend detects whether the value is a UUID (resolve from S3) or a full URL (use directly). Heuristic: try parsing as UUID; if it succeeds, treat as S3 key; otherwise, treat as URL. - (c) Reuse
image_urlwith a prefix convention. E.g.,s3:{uuid}for S3 keys, plain URL otherwise.
Recommendation: (a) — a separate column avoids ambiguity and matches the API
model (imageKey and imageUrl are separate fields on ItemInput).
CSV-Q2: Pre-upload workflow. CSV import implies all referenced images must be uploaded to S3 BEFORE the CSV is processed. What is the expected workflow?
- User uploads N images via the presigned POST endpoint (one per image), collecting N UUIDs.
- User constructs the CSV with
image_keyvalues referencing those UUIDs. - User uploads and processes the CSV.
- During processing, each row’s
image_keyis resolved and validated via HEAD.
Is this the intended sequence? Or is there a bulk image upload mechanism anticipated? (Bulk image upload is out of scope per the project description, but the CSV workflow depends on images already being in S3.)
CSV-Q3: Performance at scale. If a CSV has 1,000 rows with image_key
values, the backend performs up to 3,000 HEAD calls (3 extension probes per
row). Considerations:
- S3 HEAD requests are ~5ms each, so 3,000 calls is ~15 seconds sequentially. Parallelization could reduce this significantly.
- Alternative: use
ListObjectsV2with prefix{tenantId}/operations/item/image/{uuid}— returns all matching keys in one call. More efficient for batch resolution but different API. - Should the CSV processing path use a different (batch-optimized) resolution strategy than the single-item API path?
CSV-Q4: Error handling granularity. The existing CSV import reports per-row
success/failure (via RowResult). If a row’s image_key doesn’t resolve:
- Should the row fail with a
BuildError(same as malformedimage_urltoday)? - Should the entire batch be rejected?
- Should the error message indicate which extensions were tried?
Recommendation: Per-row failure with a descriptive error message (consistent with existing CSV validation patterns).
CSV-Q5: Mutual exclusivity. If a CSV row provides both image_url and
image_key:
- (a) Same rules as the API:
imageKeytakes precedence, fail if it doesn’t resolve (regardless ofimageUrlvalue). - (b) Reject the row outright (provide one or the other, not both).
- (c) Different rules: CSV is a batch tool, allow fallback to
imageUrl.
Recommendation: (a) — same semantics as the API for consistency. The
resolveImageKey method is shared between API and CSV paths.
Actions
Section titled “Actions”Items requiring changes to prior documents. Do not modify the referenced documents during this session — capture amendments here for later application.
| # | Document / Decision | Change Required |
|---|---|---|
| A-1 | DS-02, E-1 | Remove {itemId} from upload endpoint path. New path: POST /v1/item/item/image/upload-url (no entity-id). Rationale: item may not exist during creation. S3 key does not include entity-id (DQ-001). |
| A-2 | DS-02, L-2 / L-6 | PresignedPostResult should include imageKey: UUID as a separate field alongside objectKey: String, so the client does not need to parse the key string. |
| A-3 | DS-02, Resulting Design | Update guidance bullet for “both provided” case: if imageKey is provided and does not resolve, the operation fails — no fallback to imageUrl. (Per CQ-7 resolution.) |
| A-4 | Decision Log | Update DQ-005 to reflect corrected upload endpoint path (no entity-id) and the imageKey: UUID response field. |
| A-5 | DS-03, OQ-3 | Confirmed: upload endpoint follows the /v1/item/item/ prefix convention. Path: POST /v1/item/item/image/upload-url. |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved