From f42d3e4fd501b83aa1f40d99e2d3dc606db07af1 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Tue, 8 Sep 2026 19:30:09 -0400 Subject: [PATCH] feat: add public listing catalog boundary --- apps/api/package.json | 3 +- apps/api/src/app/build-app.js | 5 + apps/api/src/app/server.js | 2 + .../repositories/listing-repository.js | 15 + .../listings/routes/public-listing-routes.js | 32 + .../services/public-listing-service.js | 23 + .../public-listing-service.test.js | 31 + .../tests/http/public-listing-routes.test.js | 77 +++ .../integration/listing-repository.test.js | 5 + docs/API.md | 1 + docs/IMPLEMENTATION-STATUS.md | 3 +- package-lock.json | 626 +++++++++++++++++- packages/contracts/package.json | 1 + packages/contracts/public-listing.js | 17 + 14 files changed, 838 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/modules/listings/routes/public-listing-routes.js create mode 100644 apps/api/src/modules/listings/services/public-listing-service.js create mode 100644 apps/api/tests/application/public-listing-service.test.js create mode 100644 apps/api/tests/http/public-listing-routes.test.js create mode 100644 packages/contracts/public-listing.js diff --git a/apps/api/package.json b/apps/api/package.json index 58cd0b3..5b18517 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,6 +21,7 @@ "nodemailer": "^7.0.6", "otpauth": "^9.4.1", "pg": "^8.16.3", - "redis": "^5.8.2" + "redis": "^5.8.2", + "sharp": "^0.35.4" } } diff --git a/apps/api/src/app/build-app.js b/apps/api/src/app/build-app.js index 4b3c417..65cf3d8 100644 --- a/apps/api/src/app/build-app.js +++ b/apps/api/src/app/build-app.js @@ -6,6 +6,7 @@ import { registerAuthRoutes } from '../modules/auth/routes/auth-routes.js'; import { registerAdminInventoryRoutes } from '../modules/inventory/routes/admin-inventory-routes.js'; import { registerAdminImageRoutes } from '../modules/inventory/routes/admin-image-routes.js'; import { registerAdminListingRoutes } from '../modules/listings/routes/admin-listing-routes.js'; +import { registerPublicListingRoutes } from '../modules/listings/routes/public-listing-routes.js'; import { registerAdminProductRoutes } from '../modules/products/routes/admin-product-routes.js'; import { registerPublicProductRoutes } from '../modules/products/routes/public-product-routes.js'; import { registerPublicSearchRoutes } from '../modules/search/routes/public-search-routes.js'; @@ -60,6 +61,10 @@ export function buildApp(dependencies) { ...dependencies, prefix: '/api/v1', }); + app.register(registerPublicListingRoutes, { + ...dependencies, + prefix: '/api/v1', + }); return app; } \ No newline at end of file diff --git a/apps/api/src/app/server.js b/apps/api/src/app/server.js index 930a5c6..7508b08 100644 --- a/apps/api/src/app/server.js +++ b/apps/api/src/app/server.js @@ -22,6 +22,7 @@ import { createLocalImageStorage } from '../infrastructure/images/local-image-st import { createConditionRepository } from '../modules/inventory/repositories/condition-repository.js'; import { createListingRepository } from '../modules/listings/repositories/listing-repository.js'; import { createListingService } from '../modules/listings/services/listing-service.js'; +import { createPublicListingService } from '../modules/listings/services/public-listing-service.js'; import { createProductRepository } from '../modules/products/repositories/product-repository.js'; import { createProductService } from '../modules/products/services/product-service.js'; import { createPublicProductService } from '../modules/products/services/public-product-service.js'; @@ -86,6 +87,7 @@ async function startServer() { inventoryImageRepository, }), listingService: createListingService({ listingRepository, inventoryImageRepository }), + publicListingService: createPublicListingService({ listingRepository }), productService: createProductService({ productRepository }), publicProductService: createPublicProductService({ productRepository }), searchService: createSearchService({ listingRepository }), diff --git a/apps/api/src/modules/listings/repositories/listing-repository.js b/apps/api/src/modules/listings/repositories/listing-repository.js index 885aaaa..1491e78 100644 --- a/apps/api/src/modules/listings/repositories/listing-repository.js +++ b/apps/api/src/modules/listings/repositories/listing-repository.js @@ -121,6 +121,21 @@ export function createListingRepository(db) { }; }, + async findPublicById(listingId) { + const [result] = await db + .select({ listing: listings }) + .from(listings) + .innerJoin(inventoryItems, eq(inventoryItems.id, listings.inventoryItemId)) + .innerJoin(products, eq(products.id, inventoryItems.productId)) + .where(and( + eq(listings.id, listingId), + inArray(listings.status, ['AVAILABLE', 'PENDING']), + eq(inventoryItems.status, 'ACTIVE'), + )); + + return result?.listing ?? null; + }, + async updateStatus(listingId, status) { const [updatedListing] = await db .update(listings) diff --git a/apps/api/src/modules/listings/routes/public-listing-routes.js b/apps/api/src/modules/listings/routes/public-listing-routes.js new file mode 100644 index 0000000..9b5029e --- /dev/null +++ b/apps/api/src/modules/listings/routes/public-listing-routes.js @@ -0,0 +1,32 @@ +import { toPublicListingDto } from '@pokemon-inventory/contracts/public-listing'; + +function coerceNumber(value, fallback) { + if (value === undefined || value === null || value === '') { + return fallback; + } + + const parsed = Number.parseInt(String(value), 10); + return Number.isNaN(parsed) ? fallback : parsed; +} + +export async function registerPublicListingRoutes(app, options) { + const { publicListingService } = options; + + app.get('/public/listings', async (request) => { + const result = await publicListingService.list({ + page: coerceNumber(request.query.page, 1), + limit: coerceNumber(request.query.limit, 20), + }); + + return { + data: { + ...result, + items: result.items.map(toPublicListingDto), + }, + }; + }); + + app.get('/public/listings/:id', async (request) => ({ + data: toPublicListingDto(await publicListingService.getById(request.params.id)), + })); +} \ No newline at end of file diff --git a/apps/api/src/modules/listings/services/public-listing-service.js b/apps/api/src/modules/listings/services/public-listing-service.js new file mode 100644 index 0000000..a0ac07a --- /dev/null +++ b/apps/api/src/modules/listings/services/public-listing-service.js @@ -0,0 +1,23 @@ +import { createApplicationError } from '../../../shared/errors/application-error.js'; + +export function createPublicListingService({ listingRepository }) { + return { + async list(input = {}) { + return listingRepository.searchPublic({ + query: '', + page: input.page, + limit: input.limit, + }); + }, + + async getById(listingId) { + const listing = await listingRepository.findPublicById(listingId); + + if (!listing) { + throw createApplicationError('LISTING_NOT_FOUND', 'Listing was not found.', 404); + } + + return listing; + }, + }; +} \ No newline at end of file diff --git a/apps/api/tests/application/public-listing-service.test.js b/apps/api/tests/application/public-listing-service.test.js new file mode 100644 index 0000000..2910a6f --- /dev/null +++ b/apps/api/tests/application/public-listing-service.test.js @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createPublicListingService } from '../../src/modules/listings/services/public-listing-service.js'; + +test('lists visible public listings through the shared repository search boundary', async () => { + const service = createPublicListingService({ + listingRepository: { + async searchPublic(filters) { + assert.deepEqual(filters, { query: '', page: 2, limit: 5 }); + return { items: [{ id: 'listing-1' }], total: 1 }; + }, + }, + }); + + assert.deepEqual(await service.list({ page: 2, limit: 5 }), { + items: [{ id: 'listing-1' }], + total: 1, + }); +}); + +test('returns a not-found application error for a hidden public listing', async () => { + const service = createPublicListingService({ + listingRepository: { async findPublicById() { return null; } }, + }); + + await assert.rejects( + () => service.getById('listing-1'), + (error) => error.code === 'LISTING_NOT_FOUND' && error.statusCode === 404, + ); +}); \ No newline at end of file diff --git a/apps/api/tests/http/public-listing-routes.test.js b/apps/api/tests/http/public-listing-routes.test.js new file mode 100644 index 0000000..c4f9153 --- /dev/null +++ b/apps/api/tests/http/public-listing-routes.test.js @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { buildApp } from '../../src/app/build-app.js'; + +function createTestApp() { + return buildApp({ + publicListingService: { + async list() { + return { + items: [{ + id: 'listing-1', + inventoryItemId: 'private-inventory-id', + productName: 'Pikachu Plush', + categoryId: 'category-1', + conditionId: 'condition-1', + askingPriceMinor: 2499, + status: 'PENDING', + }], + pagination: { page: 1, limit: 20, total: 1, totalPages: 1 }, + }; + }, + async getById(id) { + if (id !== 'listing-1') { + const error = new Error('Listing was not found.'); + error.code = 'LISTING_NOT_FOUND'; + error.statusCode = 404; + throw error; + } + + return { + id, + inventoryItemId: 'private-inventory-id', + productName: 'Pikachu Plush', + categoryId: 'category-1', + conditionId: 'condition-1', + askingPriceMinor: 2499, + status: 'PENDING', + }; + }, + }, + }); +} + +test('returns public listing collections and omits inventory identity', async (testContext) => { + const app = createTestApp(); + testContext.after(() => app.close()); + + const response = await app.inject({ + method: 'GET', + url: '/api/v1/public/listings?page=1&limit=20', + }); + + assert.equal(response.statusCode, 200); + assert.equal(response.json().data.items[0].id, 'listing-1'); + assert.equal('inventoryItemId' in response.json().data.items[0], false); + assert.equal(response.json().data.pagination.total, 1); +}); + +test('returns a public listing detail or a not-found response', async (testContext) => { + const app = createTestApp(); + testContext.after(() => app.close()); + + const detailResponse = await app.inject({ + method: 'GET', + url: '/api/v1/public/listings/listing-1', + }); + const missingResponse = await app.inject({ + method: 'GET', + url: '/api/v1/public/listings/hidden-listing', + }); + + assert.equal(detailResponse.statusCode, 200); + assert.equal('inventoryItemId' in detailResponse.json().data, false); + assert.equal(missingResponse.statusCode, 404); + assert.equal(missingResponse.json().error.code, 'LISTING_NOT_FOUND'); +}); \ No newline at end of file diff --git a/apps/api/tests/integration/listing-repository.test.js b/apps/api/tests/integration/listing-repository.test.js index aa91920..3d2ee51 100644 --- a/apps/api/tests/integration/listing-repository.test.js +++ b/apps/api/tests/integration/listing-repository.test.js @@ -141,6 +141,10 @@ test('searches public listings with pagination and hides sold items by default', assert.equal(visible.total, 1); assert.equal(visible.items.length, 1); assert.equal(visible.items[0].status, 'PENDING'); + assert.equal( + (await listingRepository.findPublicById(visible.items[0].id))?.id, + visible.items[0].id, + ); await database.db .update(listings) @@ -153,6 +157,7 @@ test('searches public listings with pagination and hides sold items by default', const hidden = await listingRepository.searchPublic({ page: 1, limit: 10 }); assert.equal(hidden.total, 0); + assert.equal(await listingRepository.findPublicById(visible.items[0].id), null); const included = await listingRepository.searchPublic({ page: 1, diff --git a/docs/API.md b/docs/API.md index 6252259..c1906ca 100644 --- a/docs/API.md +++ b/docs/API.md @@ -19,6 +19,7 @@ GET /api/v1/public/search GET /api/v1/public/products/:slug GET /api/v1/public/inventory GET /api/v1/public/listings +GET /api/v1/public/listings/:id GET /api/v1/public/sellers GET /api/v1/public/currencies GET /api/v1/public/exchange-rates diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 7fce3bb..f9a6ae1 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -93,6 +93,7 @@ docker context use orbstack - Public search is available at `GET /api/v1/public/search` through a normalized SearchService boundary with pagination, price filters, text matching, and sold-item visibility controls. - Public search responses include `items` plus `pagination` metadata (`page`, `limit`, `total`, and `totalPages`); pending listings are visible by default and sold listings require an explicit filter. - Public product pages are available at `GET /api/v1/public/products/:slug`; responses use a public DTO and require an active physical item with an available or pending listing. +- Public listing collection and detail reads are available at `GET /api/v1/public/listings` and `GET /api/v1/public/listings/:id`; collection responses include pagination metadata and detail reads hide internal inventory identity. - `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. - The worker owns an image derivative job runner with explicit processing, completion, and failure outcomes; queue transport and persistent job records remain. @@ -107,7 +108,7 @@ 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 87 tests; worker tests passed 2 tests; `npm run test:integration` passed 16 tests against OrbStack PostgreSQL and Redis. +Latest result: `npm test` passed 91 tests; worker tests passed 2 tests; `npm run test:integration` passed 16 tests against OrbStack PostgreSQL and Redis. ## Next Milestone diff --git a/package-lock.json b/package-lock.json index eeff30d..fc1c7ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "nodemailer": "^7.0.6", "otpauth": "^9.4.1", "pg": "^8.16.3", - "redis": "^5.8.2" + "redis": "^5.8.2", + "sharp": "^0.35.4" } }, "node_modules/@drizzle-team/brocli": { @@ -37,6 +38,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1120,6 +1131,554 @@ "ipaddr.js": "^2.1.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@noble/hashes": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", @@ -1344,6 +1903,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -2122,6 +2690,55 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2188,6 +2805,13 @@ "node": ">=20" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.23.13", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d6dce0c..f3f8cfa 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -8,6 +8,7 @@ "./image": "./image.js", "./listing": "./listing.js", "./product": "./product.js", + "./public-listing": "./public-listing.js", "./public-product": "./public-product.js" } } \ No newline at end of file diff --git a/packages/contracts/public-listing.js b/packages/contracts/public-listing.js new file mode 100644 index 0000000..ac239d0 --- /dev/null +++ b/packages/contracts/public-listing.js @@ -0,0 +1,17 @@ +function toIsoString(value) { + return value instanceof Date ? value.toISOString() : value; +} + +export function toPublicListingDto(listing) { + return { + id: listing.id, + productName: listing.productName, + categoryId: listing.categoryId, + conditionId: listing.conditionId, + askingPriceMinor: listing.askingPriceMinor, + status: listing.status, + notes: listing.notes ?? null, + createdAt: toIsoString(listing.createdAt), + updatedAt: toIsoString(listing.updatedAt), + }; +} \ No newline at end of file