diff --git a/API.brand.md b/API.brand.md index 65827d2..5283b90 100644 --- a/API.brand.md +++ b/API.brand.md @@ -39,7 +39,7 @@ Creates a brand for the user's selected company. **Returns** -`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The created brand. +`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The created brand. **Throws** @@ -84,24 +84,27 @@ await store.brand.delete(42, 3); *Source: `src/store/brand/list.js`* -Lists the brands of the user's selected company. +Lists the brands of the user's selected company, sorted by name. **Parameters** | Name | Type | Description | | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | +| `[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). | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise}>>` — The brands. +`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. **Example** ```js -const brands = await store.brand.list(42); -// => [{ id, name, gsprpinfo, pics: [{ sort, fullHash, thumbW, thumbH, thumbHash }] }] +const result = await store.brand.list(42, { offset: 0, limit: 10 }); +// => { brands: [{ id, name, gsprpinfo, pics: [...] }], total: 25, offset: 0, limit: 10 } ``` ## `brand.update` @@ -154,7 +157,7 @@ Creates or updates a brand for the user's selected company, keyed by name. **Returns** -`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand. +`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand. **Throws** diff --git a/API.md b/API.md index 5714dcf..7b42b97 100644 --- a/API.md +++ b/API.md @@ -130,11 +130,11 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>` - [brand](API.brand.md) - `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>` - - `create(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` + - `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, 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}>` - `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<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` + - `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) - `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` diff --git a/src/providers/mssql/queries/brand.js b/src/providers/mssql/queries/brand.js index 2919f06..b7ac7e2 100644 --- a/src/providers/mssql/queries/brand.js +++ b/src/providers/mssql/queries/brand.js @@ -5,8 +5,6 @@ SELECT b.gsprpinfo, ( SELECT - bp.id, - bp.sort, p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH, @@ -22,6 +20,35 @@ WHERE b.company_id = @companyId ORDER BY b.name `; +export const COUNT_BY_COMPANY = ` +SELECT COUNT(*) AS total +FROM brand b +WHERE b.company_id = @companyId +`; + +export const LIST_BY_COMPANY_PAGED = ` +SELECT + b.id, + b.name, + b.gsprpinfo, + ( + SELECT + p.hash AS fullHash, + p.thumbw AS thumbW, + p.thumbh AS thumbH, + p.thumbhash AS thumbHash + 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, @@ -29,8 +56,6 @@ SELECT b.gsprpinfo, ( SELECT - bp.id, - bp.sort, p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH, diff --git a/src/providers/sqlite3/queries/brand.js b/src/providers/sqlite3/queries/brand.js index 4c45403..b72a55c 100644 --- a/src/providers/sqlite3/queries/brand.js +++ b/src/providers/sqlite3/queries/brand.js @@ -4,6 +4,7 @@ import { translateQueries } from './translate.js'; const queries = translateQueries(source, 'brand'); export const { CREATE, DELETE_BY_COMPANY_AND_ID, UPDATE_BY_ID, GET_BY_ID_AND_COMPANY, + COUNT_BY_COMPANY, } = queries; export const LIST_BY_COMPANY = ` @@ -13,15 +14,13 @@ SELECT b.gsprpinfo, ( SELECT COALESCE(json_group_array(json_object( - 'id', id, - 'sort', sort, 'fullHash', hash, 'thumbW', thumbw, 'thumbH', thumbh, 'thumbHash', thumbhash )), '[]') FROM ( - SELECT bp.id, bp.sort, p.hash, p.thumbw, p.thumbh, p.thumbhash + SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash FROM brandpic bp INNER JOIN pic p ON p.id = bp.pic_id WHERE bp.brand_id = b.id @@ -33,6 +32,32 @@ WHERE b.company_id = @companyId ORDER BY b.name `; +export const LIST_BY_COMPANY_PAGED = ` +SELECT + b.id, + b.name, + b.gsprpinfo, + ( + SELECT COALESCE(json_group_array(json_object( + 'fullHash', hash, + 'thumbW', thumbw, + 'thumbH', thumbh, + 'thumbHash', thumbhash + )), '[]') + FROM ( + SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash + 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, @@ -40,15 +65,13 @@ SELECT b.gsprpinfo, ( SELECT COALESCE(json_group_array(json_object( - 'id', id, - 'sort', sort, 'fullHash', hash, 'thumbW', thumbw, 'thumbH', thumbh, 'thumbHash', thumbhash )), '[]') FROM ( - SELECT bp.id, bp.sort, p.hash, p.thumbw, p.thumbh, p.thumbhash + SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash FROM brandpic bp INNER JOIN pic p ON p.id = bp.pic_id WHERE bp.brand_id = b.id diff --git a/src/store/brand/addImage.js b/src/store/brand/addImage.js index 12f3fb0..528bd16 100644 --- a/src/store/brand/addImage.js +++ b/src/store/brand/addImage.js @@ -82,11 +82,8 @@ export default function addImage(provider) { picId, sort, }); - const brandPicId = brandPicResult.rows[0]?.id ?? brandPicResult.insertId; const picObj = { - id: brandPicId, - sort, fullHash, thumbW: processed.thumb.w, thumbH: processed.thumb.h, diff --git a/src/store/brand/create.js b/src/store/brand/create.js index 6629e6a..1963823 100644 --- a/src/store/brand/create.js +++ b/src/store/brand/create.js @@ -26,7 +26,7 @@ export default function create(provider) { * @param {string} input.name - The brand name. * @param {string} [input.gsprpinfo] - Optional GSPRP info. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The created brand. + * @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The created brand. * @throws {Error} If the brand is invalid or already exists. * * @example diff --git a/src/store/brand/list.js b/src/store/brand/list.js index 23c8aa1..02de6bc 100644 --- a/src/store/brand/list.js +++ b/src/store/brand/list.js @@ -1,5 +1,13 @@ -import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/brand.js'; -import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/brand.js'; +import { + COUNT_BY_COMPANY as MSSQL_COUNT, + LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY, + LIST_BY_COMPANY_PAGED as MSSQL_LIST_PAGED, +} 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, +} from '../../providers/sqlite3/queries/brand.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; @@ -11,32 +19,63 @@ function parsePics(raw) { export default function list(provider) { const access = resolveSelectedCompanyAccess(provider); - const sql = - provider?.dialect === 'sqlite3' - ? SQLITE_LIST_BY_COMPANY - : MSSQL_LIST_BY_COMPANY; + const isSqlite = provider?.dialect === 'sqlite3'; + 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; /** - * Lists the brands of the user's selected company. + * Lists the brands of the user's selected company, sorted by name. * * @param {number} googleAuthId - The id of the Google auth user. + * @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 {object} [tx] - Optional transaction to run within. - * @returns {Promise}>>} The brands. + * @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. * * @example - * const brands = await store.brand.list(42); - * // => [{ id, name, gsprpinfo, pics: [{ sort, fullHash, thumbW, thumbH, thumbHash }] }] + * const result = await store.brand.list(42, { offset: 0, limit: 10 }); + * // => { brands: [{ id, name, gsprpinfo, pics: [...] }], total: 25, offset: 0, limit: 10 } */ - return async (googleAuthId, tx) => { - const { companyId } = await access(googleAuthId, tx); + return async (googleAuthId, opts, tx) => { + let offset = 0; + let limit = 10; + let resolvedTx = tx; - const result = await makeRunner(provider, tx).query(sql, { companyId }); + if (opts != null && typeof opts === 'object' && !opts._isTx) { + const parsedOffset = Number(opts.offset ?? 0); + const parsedLimit = Number(opts.limit ?? 10); + if (!Number.isInteger(parsedOffset) || parsedOffset < 0) { + throw new Error('Invalid offset'); + } + if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) { + throw new Error('Invalid limit'); + } + offset = parsedOffset; + limit = parsedLimit; + } else if (opts != null) { + resolvedTx = opts; + } - return result.rows.map((row) => ({ - id: row.id, - name: row.name, - gsprpinfo: row.gsprpinfo, - pics: parsePics(row.picsJson), - })); + const runner = makeRunner(provider, resolvedTx); + const { companyId } = await access(googleAuthId, runner); + + const [countResult, listResult] = await Promise.all([ + runner.query(sqlCount, { companyId }), + runner.query(sqlPaged, { companyId, offset, limit }), + ]); + + return { + brands: listResult.rows.map((row) => ({ + id: row.id, + name: row.name, + gsprpinfo: row.gsprpinfo, + pics: parsePics(row.picsJson), + })), + total: Number(countResult.rows[0]?.total ?? 0), + offset, + limit, + }; }; } diff --git a/src/store/brand/upsert.js b/src/store/brand/upsert.js index 3d54415..b1298d7 100644 --- a/src/store/brand/upsert.js +++ b/src/store/brand/upsert.js @@ -34,7 +34,7 @@ export default function upsert(provider) { * @param {string} input.name - The brand name. * @param {string} [input.gsprpinfo] - Optional GSPRP info. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The upserted brand. + * @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The upserted brand. * @throws {Error} If the brand is invalid. * * @example