feat: add image derivative orchestration

Define thumbnail, medium, and large WebP derivative variants behind an injected renderer and write them through ImageStorage with deterministic keys. Add focused tests and document the remaining worker-backed renderer. Verification: npm test passed 70 tests and integration passed 14 tests. No build script is configured.
This commit is contained in:
2026-09-08 17:40:07 -04:00
parent f7152bcff3
commit 961ed44055
4 changed files with 79 additions and 2 deletions
@@ -0,0 +1,32 @@
const variants = Object.freeze([
{ name: 'thumbnail', maxWidth: 320 },
{ name: 'medium', maxWidth: 1280 },
{ name: 'large', maxWidth: 2400 },
]);
function derivativeKey(sourceKey, variantName) {
const extensionIndex = sourceKey.lastIndexOf('.');
const base = extensionIndex === -1 ? sourceKey : sourceKey.slice(0, extensionIndex);
return `${base}-${variantName}.webp`;
}
export function createImageDerivativeProcessor({ imageStorage, renderer }) {
if (typeof renderer !== 'function') {
throw new TypeError('Image derivative renderer is required.');
}
return {
async process({ sourceKey, data }) {
const results = [];
for (const variant of variants) {
const derivativeData = await renderer({ data, maxWidth: variant.maxWidth });
const key = derivativeKey(sourceKey, variant.name);
await imageStorage.put({ key, data: derivativeData });
results.push({ name: variant.name, key, maxWidth: variant.maxWidth });
}
return results;
},
};
}
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createImageDerivativeProcessor } from '../../src/infrastructure/images/image-derivative-processor.js';
test('renders and stores thumbnail, medium, and large derivatives in order', async () => {
const renderedWidths = [];
const stored = [];
const processor = createImageDerivativeProcessor({
imageStorage: {
async put(value) { stored.push(value); },
},
async renderer({ data, maxWidth }) {
renderedWidths.push(maxWidth);
return Buffer.concat([data, Buffer.from(`-${maxWidth}`)]);
},
});
const results = await processor.process({
sourceKey: 'inventory/item/front.jpg',
data: Buffer.from('source'),
});
assert.deepEqual(renderedWidths, [320, 1280, 2400]);
assert.deepEqual(results.map(({ name, key }) => ({ name, key })), [
{ name: 'thumbnail', key: 'inventory/item/front-thumbnail.webp' },
{ name: 'medium', key: 'inventory/item/front-medium.webp' },
{ name: 'large', key: 'inventory/item/front-large.webp' },
]);
assert.equal(stored.length, 3);
assert.equal(stored[1].data.toString(), 'source-1280');
});
test('requires a renderer implementation', () => {
assert.throws(
() => createImageDerivativeProcessor({ imageStorage: {} }),
/renderer is required/,
);
});
+5
View File
@@ -68,6 +68,11 @@ 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.
## Metadata
Store:
+3 -2
View File
@@ -91,6 +91,7 @@ docker context use orbstack
- Listing reads are seller-scoped and return only admin listing DTO fields.
- Item-owned image metadata can be added through a seller-scoped, CSRF-protected admin route; publication uses the persisted image count.
- `ImageStorage`, traversal-safe local storage, multipart upload transport, and binary signature validation are implemented; derivative processing remains.
- Derivative orchestration now defines thumbnail, medium, and large WebP storage outputs behind an injected renderer; the concrete worker-backed renderer remains.
- Image metadata limits are enforced in the service boundary: supported MIME types, 10 MiB default file size, and 10,000-pixel maximum dimensions.
### Verified Baseline
@@ -102,13 +103,13 @@ docker context use orbstack
| HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management |
| Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling |
Latest result: `npm test` passed 68 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis.
Latest result: `npm test` passed 70 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis.
## Next Milestone
Complete the inventory/listing integrity boundary before starting pricing and acquisition.
1. Add processing/derivative lifecycle for persisted image metadata.
1. Add a portable decoder/encoder and worker-backed derivative processing lifecycle.
2. Begin acquisition and pricing foundations after the listing/image boundary is complete.
Acceptance criteria: listing reads and lifecycle writes preserve seller ownership, CSRF, inventory status, and publication prerequisites.