From f840a12b78ef4765610b3cbbd4d31d18133abc7d Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 9 Oct 2026 19:02:19 +0200 Subject: [PATCH] u --- API.brand.md | 3 ++- API.md | 2 +- scripts/test-sqlite-storage.js | 24 +++++++++++++++++++ src/providers/mssql/queries/brand.js | 26 ++++++++++++++++++++ src/providers/sqlite3/queries/brand.js | 33 ++++++++++++++++++++++++++ src/store/brand/list.js | 27 ++++++++++++++++++--- 6 files changed, 110 insertions(+), 5 deletions(-) diff --git a/API.brand.md b/API.brand.md index faafa3f..78ea210 100644 --- a/API.brand.md +++ b/API.brand.md @@ -94,11 +94,12 @@ Lists the brands of the user's selected company, sorted by name. | `[opts]` | `object` | Optional pagination options. | | `[opts.offset=0]` | `number` | Number of brands to skip. | | `[opts.limit=10]` | `number` | Number of brands to return (max 100). | +| `[opts.includeSmallThumbs=false]` | `boolean` | Include 40px-edge small thumbnail dimensions and base64 AVIF data. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>, total: number, offset: number, limit: number}>` — The brands plus the total unpaged count. +`Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>` — The brands plus the total unpaged count. Small-thumbnail fields are present only when `includeSmallThumbs` is true; the binary is base64-encoded AVIF data. **Example** diff --git a/API.md b/API.md index da3282e..af05dd5 100644 --- a/API.md +++ b/API.md @@ -132,7 +132,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array}>` - `create(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - - `list(googleAuthId: number, opts?: {offset?: number, limit?: number}, tx?: object): Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>, total: number, offset: number, limit: number}>` + - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, includeSmallThumbs?: boolean}, tx?: object): Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>` - `update(googleAuthId: number, id: number, input: {name?: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>` - `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` - [customergroup](API.customergroup.md) diff --git a/scripts/test-sqlite-storage.js b/scripts/test-sqlite-storage.js index 1bbdbe6..730028d 100644 --- a/scripts/test-sqlite-storage.js +++ b/scripts/test-sqlite-storage.js @@ -214,6 +214,30 @@ async function runTests() { assert.equal(smallThumbMetadata.format, 'heif'); assert.equal(smallThumbMetadata.width, 40); assert.equal(smallThumbMetadata.height, 27); + + const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' }); + await store.brand.addImage(googleAuthId, brand.id, sampleImage); + const listedBrands = await store.brand.list(googleAuthId, { offset: 0, limit: 100 }); + const listedBrand = listedBrands.brands.find((item) => item.id === brand.id); + assert(listedBrand, 'Brand list should include the created brand'); + assert.equal(listedBrand.pics.length, 1); + assert(!('smallThumbW' in listedBrand.pics[0]), 'Small-thumb fields should be omitted by default'); + + const listedBrandsWithSmallThumbs = await store.brand.list(googleAuthId, { + offset: 0, + limit: 100, + includeSmallThumbs: true, + }); + const listedBrandWithSmallThumbs = listedBrandsWithSmallThumbs.brands.find((item) => item.id === brand.id); + assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand'); + assert.equal(listedBrandWithSmallThumbs.pics.length, 1); + const listedPic = listedBrandWithSmallThumbs.pics[0]; + assert.equal(listedPic.smallThumbW, 40); + assert.equal(listedPic.smallThumbH, 27); + assert.equal(typeof listedPic.smallThumbBinary, 'string'); + const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64'); + assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary); + console.log('Article pic added with hash:', fullHash); const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif'); diff --git a/src/providers/mssql/queries/brand.js b/src/providers/mssql/queries/brand.js index b7ac7e2..54e5ca7 100644 --- a/src/providers/mssql/queries/brand.js +++ b/src/providers/mssql/queries/brand.js @@ -49,6 +49,32 @@ ORDER BY b.name OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY `; +export const LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS = ` +SELECT + b.id, + b.name, + b.gsprpinfo, + ( + SELECT + p.hash AS fullHash, + p.thumbw AS thumbW, + p.thumbh AS thumbH, + p.thumbhash AS thumbHash, + p.smallthumbw AS smallThumbW, + p.smallthumbh AS smallThumbH, + p.smallthumbbinary AS smallThumbBinary + FROM brandpic bp + INNER JOIN pic p ON p.id = bp.pic_id + WHERE bp.brand_id = b.id + ORDER BY bp.sort ASC, bp.id ASC + FOR JSON PATH + ) AS picsJson +FROM brand b +WHERE b.company_id = @companyId +ORDER BY b.name +OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY +`; + export const GET_BY_COMPANY_AND_NAME = ` SELECT b.id, diff --git a/src/providers/sqlite3/queries/brand.js b/src/providers/sqlite3/queries/brand.js index b72a55c..b2ac20e 100644 --- a/src/providers/sqlite3/queries/brand.js +++ b/src/providers/sqlite3/queries/brand.js @@ -58,6 +58,39 @@ ORDER BY b.name LIMIT @limit OFFSET @offset `; +export const LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS = ` +SELECT + b.id, + b.name, + b.gsprpinfo, + ( + SELECT COALESCE(json_group_array(json_object( + 'fullHash', hash, + 'thumbW', thumbw, + 'thumbH', thumbh, + 'thumbHash', thumbhash, + 'smallThumbW', smallthumbw, + 'smallThumbH', smallthumbh, + 'smallThumbBinaryHex', CASE + WHEN smallthumbbinary IS NULL THEN NULL + ELSE hex(smallthumbbinary) + END + )), '[]') + FROM ( + SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash, + p.smallthumbw, p.smallthumbh, p.smallthumbbinary + FROM brandpic bp + INNER JOIN pic p ON p.id = bp.pic_id + WHERE bp.brand_id = b.id + ORDER BY bp.sort, bp.id + ) + ) AS picsJson +FROM brand b +WHERE b.company_id = @companyId +ORDER BY b.name +LIMIT @limit OFFSET @offset +`; + export const GET_BY_COMPANY_AND_NAME = ` SELECT b.id, diff --git a/src/store/brand/list.js b/src/store/brand/list.js index 02de6bc..dce60b2 100644 --- a/src/store/brand/list.js +++ b/src/store/brand/list.js @@ -2,11 +2,13 @@ import { COUNT_BY_COMPANY as MSSQL_COUNT, LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY, LIST_BY_COMPANY_PAGED as MSSQL_LIST_PAGED, + LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS as MSSQL_LIST_PAGED_WITH_SMALL_THUMBS, } from '../../providers/mssql/queries/brand.js'; import { COUNT_BY_COMPANY as SQLITE_COUNT, LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY, LIST_BY_COMPANY_PAGED as SQLITE_LIST_PAGED, + LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS as SQLITE_LIST_PAGED_WITH_SMALL_THUMBS, } from '../../providers/sqlite3/queries/brand.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -14,7 +16,17 @@ import makeRunner from '../runner.js'; function parsePics(raw) { if (!raw) return []; const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; - return Array.isArray(parsed) ? parsed : []; + if (!Array.isArray(parsed)) return []; + return parsed.map((pic) => { + const { smallThumbBinaryHex, ...result } = pic; + if (smallThumbBinaryHex === undefined) return result; + return { + ...result, + smallThumbBinary: smallThumbBinaryHex === null + ? null + : Buffer.from(smallThumbBinaryHex, 'hex').toString('base64'), + }; + }); } export default function list(provider) { @@ -23,6 +35,9 @@ export default function list(provider) { const sql = isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY; const sqlCount = isSqlite ? SQLITE_COUNT : MSSQL_COUNT; const sqlPaged = isSqlite ? SQLITE_LIST_PAGED : MSSQL_LIST_PAGED; + const sqlPagedWithSmallThumbs = isSqlite + ? SQLITE_LIST_PAGED_WITH_SMALL_THUMBS + : MSSQL_LIST_PAGED_WITH_SMALL_THUMBS; /** * Lists the brands of the user's selected company, sorted by name. @@ -31,8 +46,9 @@ export default function list(provider) { * @param {object} [opts] - Optional pagination options. * @param {number} [opts.offset=0] - Number of brands to skip. * @param {number} [opts.limit=10] - Number of brands to return (max 100). + * @param {boolean} [opts.includeSmallThumbs=false] - Include 40px-edge small thumbnail dimensions and base64 AVIF data. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>, total: number, offset: number, limit: number}>} The brands plus the total unpaged count. + * @returns {Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>} The brands plus the total unpaged count. Small-thumbnail fields are present only when `includeSmallThumbs` is true; the binary is base64-encoded AVIF data. * * @example * const result = await store.brand.list(42, { offset: 0, limit: 10 }); @@ -41,6 +57,7 @@ export default function list(provider) { return async (googleAuthId, opts, tx) => { let offset = 0; let limit = 10; + let includeSmallThumbs = false; let resolvedTx = tx; if (opts != null && typeof opts === 'object' && !opts._isTx) { @@ -54,6 +71,7 @@ export default function list(provider) { } offset = parsedOffset; limit = parsedLimit; + includeSmallThumbs = opts.includeSmallThumbs === true; } else if (opts != null) { resolvedTx = opts; } @@ -63,7 +81,10 @@ export default function list(provider) { const [countResult, listResult] = await Promise.all([ runner.query(sqlCount, { companyId }), - runner.query(sqlPaged, { companyId, offset, limit }), + runner.query( + includeSmallThumbs ? sqlPagedWithSmallThumbs : sqlPaged, + { companyId, offset, limit }, + ), ]); return {