This commit is contained in:
seb
2026-10-23 22:22:32 +02:00
parent 82ab27ef2b
commit c172f2f4b1
6 changed files with 285 additions and 91 deletions

View File

@@ -1,37 +1,64 @@
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY, LIST_BY_ARTICLE_ID_LIMITED as MSSQL_QUERY_LIMITED } from '../../providers/mssql/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY, LIST_BY_ARTICLE_ID_LIMITED as SQLITE_QUERY_LIMITED } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function getHistoryBySku(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY;
const sqlLimited = isSqlite ? SQLITE_QUERY_LIMITED : MSSQL_QUERY_LIMITED;
/**
* Returns the change history of an article by its SKU.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [opts] - Optional options.
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
* @param {object} [opts] - Optional pagination and filter options.
* @param {number} [opts.offset=0] - Number of entries to skip.
* @param {number} [opts.limit=50] - Number of entries to return.
* @param {string} [opts.search] - Optional search filter for history text.
* @param {number} [opts.seekMonths] - Negative integer cutoff months ago.
* @param {boolean} [opts.paged] - Explicitly return paged result object.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>} The article history entries.
* @returns {Promise<{history: Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>} The article history entries.
*
* @example
* const result = await store.article.getHistoryBySku(42, 'SKU-1', { offset: 0, limit: 50, search: 'Tee' });
* // => { history: [...], total, unfilteredTotal, offset, limit }
*
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
* // => [{ id, companyuserId, date, text, meta }]
*
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
* // => up to 5 most recent entries
*/
return async (googleAuthId, sku, opts, tx) => {
// Support legacy call signature: getHistoryBySku(googleAuthId, sku, tx)
let limit = null;
let offset = 0;
let limit = 50;
let isPaged = false;
let resolvedTx = tx;
const hasFilter = opts != null && typeof opts === 'object' && !opts._isTx && (
opts.offset !== undefined ||
opts.paged === true ||
(typeof opts.search === 'string' && opts.search.trim() !== '') ||
opts.seekMonths !== undefined
);
if (opts != null && typeof opts === 'object' && !opts._isTx) {
limit = opts.limit != null ? Number(opts.limit) : null;
if (hasFilter || opts.paged === true) {
isPaged = true;
if (opts.offset !== undefined) {
const parsedOffset = Number(opts.offset);
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
throw new Error('Invalid offset');
}
offset = parsedOffset;
}
if (opts.limit !== undefined) {
const parsedLimit = Number(opts.limit);
if (!Number.isInteger(parsedLimit) || parsedLimit < 1) {
throw new Error('Invalid limit');
}
limit = parsedLimit;
}
} else if (opts.limit != null) {
limit = Number(opts.limit);
}
} else if (opts != null) {
resolvedTx = opts;
}
@@ -39,17 +66,124 @@ export default function getHistoryBySku(provider) {
const runner = makeRunner(provider, resolvedTx);
const { articleId } = await resolve(googleAuthId, sku, runner);
let result;
if (limit != null && Number.isInteger(limit) && limit > 0) {
result = await runner.query(sqlLimited, { articleId, limit });
} else {
result = await runner.query(sql, { articleId });
const conditions = ['ah.article_id = @articleId'];
const params = {
articleId,
};
if (opts?.seekMonths !== undefined) {
if (typeof opts.seekMonths !== 'number' || !Number.isInteger(opts.seekMonths)) {
throw new Error('Invalid seekMonths');
}
const d = new Date();
d.setMonth(d.getMonth() - Math.abs(opts.seekMonths));
params.dateTo = d.toISOString();
conditions.push(`ah.${isSqlite ? '"date"' : '[date]'} <= @dateTo`);
}
if (typeof opts?.search === 'string' && opts.search.trim() !== '') {
params.searchPattern = `%${opts.search.trim()}%`;
conditions.push(`ah.${isSqlite ? '"text"' : '[text]'} LIKE @searchPattern`);
}
const whereClause = conditions.join('\n AND ');
const sqlCount = `
SELECT COUNT(*) AS total
FROM articlehistory ah
WHERE ${whereClause}
`;
const sqlCountBase = `
SELECT COUNT(*) AS total
FROM articlehistory ah
WHERE ah.article_id = @articleId
`;
let sqlList;
if (isSqlite) {
sqlList = `
SELECT
ah.id,
ah.companyuser_id AS companyuserId,
ah."date",
ah."text",
ah.meta
FROM articlehistory ah
WHERE ${whereClause}
ORDER BY ah."date" DESC, ah.id DESC
${isPaged ? 'LIMIT @limit OFFSET @offset' : (limit != null && limit > 0 ? 'LIMIT @limit' : '')}
`;
} else {
sqlList = `
SELECT
${!isPaged && limit != null && limit > 0 ? 'TOP (@limit)' : ''}
ah.id,
ah.companyuser_id AS companyuserId,
ah.[date],
ah.[text],
ah.meta
FROM articlehistory ah
WHERE ${whereClause}
ORDER BY ah.[date] DESC, ah.id DESC
${isPaged ? 'OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY' : ''}
`;
}
if (isPaged) {
params.offset = offset;
params.limit = limit;
const hasUserFilters = (
opts?.seekMonths !== undefined ||
(typeof opts?.search === 'string' && opts.search.trim() !== '')
);
const [countResult, listResult, unfilteredCountResult] = await Promise.all([
runner.query(sqlCount, params),
runner.query(sqlList, params),
hasUserFilters ? runner.query(sqlCountBase, { articleId }) : null,
]);
const total = Number(countResult.rows[0]?.total ?? 0);
const unfilteredTotal = hasUserFilters
? Number(unfilteredCountResult?.rows[0]?.total ?? 0)
: total;
return {
history: listResult.rows.map((row) => ({
id: row.id,
companyuserId: row.companyuserId ?? null,
date:
row.date instanceof Date
? row.date.toISOString()
: row.date
? new Date(row.date).toISOString()
: null,
text: row.text,
meta: row.meta ?? null,
})),
total,
unfilteredTotal,
offset,
limit,
};
}
if (limit != null && limit > 0) {
params.limit = limit;
}
const result = await runner.query(sqlList, params);
return result.rows.map((row) => ({
id: row.id,
companyuserId: row.companyuserId ?? null,
date: row.date,
date:
row.date instanceof Date
? row.date.toISOString()
: row.date
? new Date(row.date).toISOString()
: null,
text: row.text,
meta: row.meta ?? null,
}));