diff --git a/API.article.md b/API.article.md index 006ee51..f372604 100644 --- a/API.article.md +++ b/API.article.md @@ -209,22 +209,26 @@ Returns the change history of an article by its SKU. | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | | `sku` | `string` | The article SKU. | -| `[opts]` | `object` | Optional options. | -| `[opts.limit]` | `number` | Maximum number of entries to return (newest first). | +| `[opts]` | `object` | Optional pagination and filter options. | +| `[opts.offset=0]` | `number` | Number of entries to skip. | +| `[opts.limit=50]` | `number` | Number of entries to return. | +| `[opts.search]` | `string` | Optional search filter for history text. | +| `[opts.seekMonths]` | `number` | Negative integer cutoff months ago. | +| `[opts.paged]` | `boolean` | Explicitly return paged result object. | | `[tx]` | `object` | Optional transaction. | **Returns** -`Promise>` — The article history entries. +`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** ```js +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 ``` ## `article.importRows` diff --git a/API.md b/API.md index 713b430..f596e62 100644 --- a/API.md +++ b/API.md @@ -112,7 +112,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise` - `getBySku(googleAuthId: number): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` - - `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise>` + - `getHistoryBySku(googleAuthId: number, sku: string, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number, paged?: boolean}, tx?: object): 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}>>` - `importRows(input: {googleAuthId: number, rows: Array}, tx?: object): Promise<{imported: Array, skipped: Array, errors: Array}>` - `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise>` - `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise>` diff --git a/package-lock.json b/package-lock.json index b378db0..390735c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "dotenv": "^18.0.8", "pdfconvert-wasm": "^0.3.0", "sharp": "^0.35.5", - "skelehash-wasm": "^0.1.0", + "skelehash-wasm": "file:../skelehash-wasm", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -32,6 +32,13 @@ } } }, + "../skelehash-wasm": { + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "sharp": "^0.35.5" + } + }, "node_modules/@azure-rest/core-client": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.9.0.tgz", @@ -184,9 +191,9 @@ } }, "node_modules/@azure/identity": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", - "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz", + "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==", "dev": true, "license": "MIT", "dependencies": { @@ -200,7 +207,7 @@ "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^6.0.0", - "open": "^10.1.0", + "open": "^11.0.0", "tslib": "^2.2.0" }, "engines": { @@ -265,22 +272,22 @@ } }, "node_modules/@azure/msal-browser": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.22.0.tgz", - "integrity": "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.26.0.tgz", + "integrity": "sha512-GYKRquzDZmZggCdStWy2bNJKnPui1upssSuznJgP+/h6Mb0ldkUkaGvaY6uSGGmZOm0CGiBQ3b5BiGy43BpXSg==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.14.1" + "@azure/msal-common": "16.14.3" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", - "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "version": "16.14.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.3.tgz", + "integrity": "sha512-tz3GvMrR9V/plb2lDdW8qaO8CPpfZevdExBKjltzzbAtyhK0eB2UIvji1aU+gkyHj3fxQ2ie+LNHyMXdmo8wOg==", "dev": true, "license": "MIT", "engines": { @@ -301,6 +308,16 @@ "node": ">=20" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@clack/core": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", @@ -340,9 +357,9 @@ } }, "node_modules/@hpcc-js/wasm-graphviz": { - "version": "1.29.2", - "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.2.tgz", - "integrity": "sha512-AkArBAZERT5OxH52/+kvCHB07cZ6ZNd9GnOvjw+S880bR88D6i27XfK6xj4dBhUDxNA2QPPMdgEcky3x4TJ3KA==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz", + "integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==", "license": "Apache-2.0" }, "node_modules/@img/colour": { @@ -894,9 +911,9 @@ } }, "node_modules/@js-joda/core": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz", - "integrity": "sha512-H8NTMRDJqad/leyv/D/A3kSOsf5/58Ydj4DJGDyaCWk9OU/zuZOLhndVffJgQjsgrn5GC0znHMHie7TfvPPG4w==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.3.0.tgz", + "integrity": "sha512-mdmjc6lJkTCORsHlMxBT9XM1yLqgtsNZmv/Hwme2v/HIb15D2XcmklZKTnynqBgsSizA9SkFvzpuksfIXhDV/w==", "dev": true, "license": "BSD-3-Clause" }, @@ -908,9 +925,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", - "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "dev": true, "license": "MIT", "dependencies": { @@ -918,9 +935,9 @@ } }, "node_modules/@types/readable-stream": { - "version": "4.0.24", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", - "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "version": "4.0.25", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.25.tgz", + "integrity": "sha512-7GsxL8Wd5X3jh73/qgW7WCnbBvGSdD8U3E4Jiuus3/8I+PN20DtlkJtwFhr/sImXfDDQ2Z03i/9wNiIF3TVSsw==", "dev": true, "license": "MIT", "dependencies": { @@ -1045,9 +1062,9 @@ "license": "BSD-3-Clause" }, "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1153,9 +1170,9 @@ } }, "node_modules/dotenv": { - "version": "18.0.8", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", - "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz", + "integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==", "license": "BSD-2-Clause", "bin": { "dotenv": "dist/index.cjs" @@ -1310,6 +1327,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -1455,9 +1485,9 @@ "license": "MIT" }, "node_modules/mssql": { - "version": "12.7.9", - "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.9.tgz", - "integrity": "sha512-ryuy8HyUpsUgg16mkjNumuGK0/kzZ9TNkKSm/lfNcb8v5RII/U4ErOquH/kAvUqXbap9BWV0EnN5JxdewGrx7g==", + "version": "12.7.11", + "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.11.tgz", + "integrity": "sha512-O24UQA++JLvMUy5T6xL73kCHP4YjLMS6DBxKFeiWyc9kZW2mrVAZ6uqly2d3hGtBTXHRFRImfM+3EdCZVE5bWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1492,19 +1522,21 @@ } }, "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1516,6 +1548,19 @@ "integrity": "sha512-FbEk+Grg0UST4kOPuN7OFfVsut1FvDOJNPS3pt4rzCS8VlFsFzD7ptg7YtFZjEOKgJ4MgqlfCgjK4RYaRBpT0Q==", "license": "GPLv3" }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1527,9 +1572,9 @@ } }, "node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz", + "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1652,17 +1697,8 @@ "license": "MIT" }, "node_modules/skelehash-wasm": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz", - "integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==", - "license": "MIT" - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "../skelehash-wasm", + "link": true }, "node_modules/string_decoder": { "version": "1.3.0", @@ -1685,9 +1721,9 @@ } }, "node_modules/tedious": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/tedious/-/tedious-20.0.0.tgz", - "integrity": "sha512-bTR0aou0Ghucf0ytvZUJjnKHGKDV8tT57jPYtEkSpfTWFe++4uR1wxJLQ4mh5wlSvAYXzmgBxbI0vaE56qigXw==", + "version": "20.3.8", + "resolved": "https://registry.npmjs.org/tedious/-/tedious-20.3.8.tgz", + "integrity": "sha512-ZrJiwyGn340Sa0V0g37Xqj5bvmJ1riYwvEtg7zdm/lpw70f2ZNIQ2fvHmntU5fEOorWDa48Jo1oKYAuUP+rh7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1699,8 +1735,7 @@ "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", - "native-duplexpair": "^1.0.0", - "sprintf-js": "^1.1.3" + "native-duplexpair": "^1.0.0" }, "engines": { "node": ">=22" @@ -1727,16 +1762,17 @@ "license": "MIT" }, "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "dev": true, "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0" + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 4aef207..6717a28 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "dotenv": "^18.0.8", "pdfconvert-wasm": "^0.3.0", "sharp": "^0.35.5", - "skelehash-wasm": "^0.1.0", + "skelehash-wasm": "file:../skelehash-wasm", "thumbhash": "^0.1.1" }, "peerDependencies": { @@ -47,4 +47,4 @@ "better-sqlite3": "^13.0.3", "mssql": "^12.7.9" } -} +} \ No newline at end of file diff --git a/src/store/article/getHistoryBySku.js b/src/store/article/getHistoryBySku.js index 4013f3d..8243174 100644 --- a/src/store/article/getHistoryBySku.js +++ b/src/store/article/getHistoryBySku.js @@ -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>} 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, })); diff --git a/test/article.test.js b/test/article.test.js index 8aef6ea..759b1d4 100644 --- a/test/article.test.js +++ b/test/article.test.js @@ -307,6 +307,26 @@ describe('store.article', () => { const limited = await store.article.getHistoryBySku(gid, art.sku, { limit: 1 }); assert.equal(limited.length, 1); }); + + it('supports pagination with total and unfilteredTotal', async () => { + const art = await createArticle(store, tenant, { name: 'PagedH0' }); + await store.article.updateBySku(gid, art.sku, { name: 'PagedH1' }); + await store.article.updateBySku(gid, art.sku, { name: 'PagedH2' }); + + const paged = await store.article.getHistoryBySku(gid, art.sku, { offset: 0, limit: 2 }); + assert.equal(typeof paged, 'object'); + assert.ok(Array.isArray(paged.history)); + assert.equal(paged.history.length, 2); + assert.equal(paged.offset, 0); + assert.equal(paged.limit, 2); + assert.equal(paged.total, 3); + assert.equal(paged.unfilteredTotal, 3); + + const filtered = await store.article.getHistoryBySku(gid, art.sku, { offset: 0, limit: 50, search: 'PagedH2' }); + assert.equal(filtered.total, 1); + assert.equal(filtered.unfilteredTotal, 3); + assert.equal(filtered.history.length, 1); + }); }); describe('listSearchIndex / listExport', () => {