Files
2026-09-11 00:39:27 -04:00

5.5 KiB

Image Management

Requirements

Multiple images are core functionality.

Use cases include:

  • card front/back
  • corners
  • edges/whitening
  • holographic details
  • merchandise angles
  • defects/details

Workflow

Upload → Validate → Store Original → Queue Processing → Generate Derivatives → Complete

Support:

  • multiple-file upload
  • drag/drop
  • preview
  • reorder
  • primary image
  • delete
  • add images later

Image count is configurable per seller through inventory.maxImagesPerItem (default 10). The API enforces this limit atomically while locking the physical inventory item, so concurrent uploads cannot exceed the configured cap.

Storage

Images are not stored in PostgreSQL.

Use an ImageStorage abstraction.

Initial:

  • Docker-mounted local storage

Future:

  • S3-compatible object storage

The API uses an ImageStorage contract with put, get, and delete operations. The current local adapter rejects absolute and traversal paths and does not overwrite an existing key. It stores bytes outside PostgreSQL; image metadata remains in inventory_images.

Processing

Validate:

  • MIME type
  • file signature where practical
  • size
  • image dimensions
  • configured image count

The metadata service currently enforces JPEG, PNG, and WebP MIME types, a 10 MiB default file-size limit, and a 10,000-pixel maximum width and height. Multipart upload validation now checks the declared MIME type against JPEG, PNG, and WebP signatures before storage. The injected image inspector reads dimensions and format from the bytes; the service validates those dimensions and the actual buffer length, then verifies decoding before writing storage or metadata. Multipart client dimensions are ignored.

Generate:

  • thumbnail
  • medium
  • large/responsive derivatives

Modern formats such as WebP/AVIF may be used where appropriate.

Retain originals where practical.

Derivative orchestration currently defines thumbnail, medium, and large WebP variants and writes them through ImageStorage using an injected renderer. The concrete decoder/encoder and worker queue remain separate infrastructure work so local development does not depend on a machine-specific binary.

The worker now owns the derivative job lifecycle and reports explicit PENDING, PROCESSING, COMPLETED, or FAILED outcomes. Queue transport and persistent job records remain deployment-specific work.

Metadata

Store:

  • storage key
  • original filename
  • MIME type
  • file size
  • dimensions
  • alt text
  • order
  • primary flag
  • processing status
  • timestamps

Alt text

Generate an initial suggestion automatically, but require the seller to be able to review/edit it.

Do not invent details not visible or supplied by the seller.

Accessibility

Public galleries must support:

  • keyboard operation
  • accessible controls
  • visible focus
  • meaningful names
  • focus management
  • screen-reader-friendly state

Primary images power cards, search results, homepage previews, and social/share metadata.

Admin error recovery

Keep the loaded gallery and alt-text drafts mounted during image actions and refreshes. Show action failures separately from list-read failures. After every action, including a failed multi-file upload or reorder, reload image metadata to reconcile any partial success without replaying writes. If that refresh fails, retain the last loaded gallery but disable mutations until an explicit reload succeeds. Failed initial loads show a reload action instead of an empty manager. Reload restores keyboard focus to the manager or the retry control. Pending operations block conflicting writes and uploads, including drag-and-drop.

Atomic adjacent moves

Move up/down uses one server operation. The repository locks the owning Inventory Item and its images, selects current order by sortOrder, createdAt, then id, and uses the domain move rule to choose the new order. An actual move assigns contiguous zero-based positions, resolving tied legacy/upload positions, within one PostgreSQL transaction. Any failure rolls back all changes. Concurrent moves within one item serialize; primary selection and image metadata are unchanged. Boundary moves are no-ops. No database migration is required.

Image update validation

Alt-text edits use the same trimmed 1,000-character limit as image creation. Explicit empty text or null may clear it; missing fields are invalid. The editor shows the limit, prevents typing past it, and flags oversized pre-existing drafts until corrected. Legacy absolute sort-order updates require a non-negative PostgreSQL integer; ordinary Move controls continue using the atomic move API.

Server-side upload inspection

The production ImageInspector uses the existing Sharp dependency behind readMetadata(data) and validateDecoding(data) methods. The service checks actual byte size before inspection, validates the 10,000-pixel dimension limit against inspected metadata, and only then requests decoding. Sharp rejects warnings/corruption and has a 100-million-input-pixel ceiling, matching the maximum permitted dimension product. The tiny decoded output is discarded; original storage and asynchronous derivatives retain their existing flow.

Malformed or undecodable images return 400 IMAGE_DECODE_INVALID; format mismatch uses IMAGE_SIGNATURE_INVALID, and inspected dimension violations use IMAGE_DIMENSIONS_INVALID. Rejections happen before storage, metadata, or job creation. No dependency or database migration is required. The separate JSON metadata-registration endpoint has no uploaded bytes to inspect and retains its existing required dimensions; multipart uploads no longer require them.