sqlite
This commit is contained in:
40
TODO.md
40
TODO.md
@@ -9,35 +9,35 @@ Replace raw `pool` usage in `src/store/**` with `provider.query()` /
|
|||||||
`provider.transaction()` / `provider.exec()` and the normalized result shape
|
`provider.transaction()` / `provider.exec()` and the normalized result shape
|
||||||
(`{ rows, rowsAffected, insertId }` instead of `recordset` / `rowsAffected[]`).
|
(`{ rows, rowsAffected, insertId }` instead of `recordset` / `rowsAffected[]`).
|
||||||
For every module:
|
For every module:
|
||||||
- [ ] `pool.request().input(...).query(SQL)` → `provider.query(SQL, params)`
|
- [x] `pool.request().input(...).query(SQL)` → `provider.query(SQL, params)`
|
||||||
- [ ] `pool.transaction(); begin; commit; catch rollback` → `provider.transaction(async (tx) => { ... })`
|
- [x] `pool.transaction(); begin; commit; catch rollback` → `provider.transaction(async (tx) => { ... })`
|
||||||
- [ ] `result.recordset` → `result.rows`; `result.rowsAffected[0]` → `result.rowsAffected`
|
- [x] `result.recordset` → `result.rows`; `result.rowsAffected[0]` → `result.rowsAffected`
|
||||||
- [ ] `request.js` helper (`(tx ?? pool).request()`) becomes obsolete — delete when unused
|
- [x] `request.js` helper (`(tx ?? pool).request()`) becomes obsolete — deleted
|
||||||
- [x] `src/store/runner.js` bridges mixed call sites: converted modules accept a
|
- [x] `src/store/runner.js` bridges mixed call sites: converted modules accept a
|
||||||
provider runner *or* a legacy mssql Transaction/`pool` (wrapped on the fly)
|
provider runner *or* a legacy mssql Transaction/`pool` (wrapped on the fly)
|
||||||
so partially converted callers keep working
|
so partially converted callers keep working
|
||||||
- [ ] `recordset[0].id` after `OUTPUT INSERTED.id` → keep, but prefer `insertId` where trivial
|
- [x] `recordset[0].id` after `OUTPUT INSERTED.id` → keep, but prefer `insertId` where trivial
|
||||||
|
|
||||||
Conversion order (bottom-up by dependency):
|
Conversion order (bottom-up by dependency):
|
||||||
- [x] `lang`, `tax` (leaf helpers, used by company create)
|
- [x] `lang`, `tax` (leaf helpers, used by company create)
|
||||||
- [x] `google_auth`
|
- [x] `google_auth`
|
||||||
- [ ] `companyuser` (resolveCompanyAccess / resolveSelectedCompanyAccess first)
|
- [x] `companyuser` (resolveCompanyAccess / resolveSelectedCompanyAccess first)
|
||||||
- [ ] `company`, `companysetting`
|
- [x] `company`, `companysetting`
|
||||||
- [ ] `customergroup`, `entity`, `entityaddress`, `entitycontact`, `entityhistory`
|
- [x] `customergroup`, `entity`, `entityaddress`, `entitycontact`, `entityhistory`
|
||||||
- [ ] `unit` (incl. `ensureDefault`), `brand`, `supplier`
|
- [x] `unit` (incl. `ensureDefault`), `brand`, `supplier`
|
||||||
- [ ] `numbers` (see §2 — stored procs!)
|
- [x] `numbers` (see §2 — stored procs!)
|
||||||
- [ ] `category` (recursive CTEs!)
|
- [x] `category` (recursive CTEs!)
|
||||||
- [ ] `article` + articletext/articlepic/articleean/price/articlehistory
|
- [x] `article` + articletext/articlepic/articleean/price/articlehistory
|
||||||
- [ ] `articlesupplier`
|
- [x] `articlesupplier`
|
||||||
- [ ] `quote`, `order`, `delivery` (shared manifest helpers: `manifestLines`, `manifestDoc`, `manifestattachment`)
|
- [x] `quote`, `order`, `delivery` (shared manifest helpers: `manifestLines`, `manifestDoc`, `manifestattachment`)
|
||||||
- [ ] `posterminal`, `pic`
|
- [x] `posterminal`, `pic`
|
||||||
- [ ] `initialize.js`, `purge.js` final wiring (drop `provider.pool` fallback from `index.js`)
|
- [x] `initialize.js`, `purge.js` final wiring (drop `provider.pool` fallback from `index.js`)
|
||||||
- [ ] Delete `src/store/request.js` once no module uses it
|
- [x] Delete `src/store/request.js` once no module uses it
|
||||||
|
|
||||||
## 2. Replace MSSQL stored procedures (blocker for SQLite)
|
## 2. Replace MSSQL stored procedures (blocker for SQLite)
|
||||||
- [ ] `numbers.nextNumber` uses proc `getnextnumberwithprefixandpadding` (output param)
|
- [x] `numbers.nextNumber` uses proc `getnextnumberwithprefixandpadding` (output param)
|
||||||
- [ ] `numbers.peekNumber` uses proc `peeknextnumberwithprefixandpadding` (output param)
|
- [x] `numbers.peekNumber` uses proc `peeknextnumberwithprefixandpadding` (output param)
|
||||||
- [ ] Implement both in plain JS inside the store layer: `SELECT next/prefix/padding`,
|
- [x] Implement both in plain JS inside the store layer: `SELECT next/prefix/padding`,
|
||||||
compute `prefix + padded next`, `UPDATE numbers SET next = next + 1 WHERE ...`.
|
compute `prefix + padded next`, `UPDATE numbers SET next = next + 1 WHERE ...`.
|
||||||
Must stay atomic: use `provider.transaction()` with row locking semantics
|
Must stay atomic: use `provider.transaction()` with row locking semantics
|
||||||
(MSSQL: `WITH (UPDLOCK, ROWLOCK)` in the SELECT; SQLite: within its write tx).
|
(MSSQL: `WITH (UPDLOCK, ROWLOCK)` in the SELECT; SQLite: within its write tx).
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3",
|
||||||
"mssql": "^12.7.2"
|
"mssql": "^12.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -984,6 +985,19 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "13.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
|
||||||
|
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "6.1.6",
|
"version": "6.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||||
@@ -1466,6 +1480,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||||
|
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/open": {
|
"node_modules/open": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"better-sqlite3": "^13.0.3",
|
||||||
"mssql": "^12.7.2"
|
"mssql": "^12.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
220
scripts/test-sqlite-storage.js
Normal file
220
scripts/test-sqlite-storage.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import LibDashStore from '../src/index.js';
|
||||||
|
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('--- Starting SQLite file and pic directory storage tests ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-sqlite-store-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'custom-storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify storage subdirectories exist
|
||||||
|
assert(existsSync(join(storageDir, 'content')), 'content dir should exist');
|
||||||
|
assert(existsSync(join(storageDir, 'thumbavif')), 'thumbavif dir should exist');
|
||||||
|
assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist');
|
||||||
|
assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist');
|
||||||
|
|
||||||
|
// Verify sqlite tables do NOT contain blob columns
|
||||||
|
const fileTableInfo = provider.db.pragma('table_info("file")');
|
||||||
|
const fileColNames = fileTableInfo.map((c) => c.name);
|
||||||
|
console.log('file table columns:', fileColNames);
|
||||||
|
assert(!fileColNames.includes('content'), 'file table should NOT have content column');
|
||||||
|
assert(!fileColNames.includes('thumbavif'), 'file table should NOT have thumbavif column');
|
||||||
|
|
||||||
|
const picTableInfo = provider.db.pragma('table_info("pic")');
|
||||||
|
const picColNames = picTableInfo.map((c) => c.name);
|
||||||
|
console.log('pic table columns:', picColNames);
|
||||||
|
assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column');
|
||||||
|
assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column');
|
||||||
|
|
||||||
|
// Setup base data: google_auth, company, unit, article, manifest
|
||||||
|
const googleAuthId = await store.google_auth.add('test@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// 1. Test Manifest Attachment (file table)
|
||||||
|
console.log('1. Testing manifestattachment with file table...');
|
||||||
|
const delivery = await store.delivery.create(googleAuthId, {
|
||||||
|
externalNumber: 'DEL-1',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
name: 'Item A',
|
||||||
|
amount: 5,
|
||||||
|
net: 10,
|
||||||
|
taxrate: 19,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifestId = delivery.id;
|
||||||
|
const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.');
|
||||||
|
const testThumb = await sharp({
|
||||||
|
create: {
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 255, g: 0, b: 0, alpha: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.avif()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const attachment = await store.manifestattachment.add(googleAuthId, manifestId, {
|
||||||
|
name: 'test-doc.txt',
|
||||||
|
mimetype: 'text/plain',
|
||||||
|
content: testContent,
|
||||||
|
thumbavif: testThumb,
|
||||||
|
thumbw: 64,
|
||||||
|
thumbh: 64,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Attachment created with hash:', attachment.hash);
|
||||||
|
|
||||||
|
// Verify files on disk have correct extensions according to mimetype
|
||||||
|
const contentPath = join(storageDir, 'content', attachment.hash + '.txt');
|
||||||
|
const thumbPath = join(storageDir, 'thumbavif', attachment.hash + '.avif');
|
||||||
|
assert(existsSync(contentPath), 'Content file must exist on disk at ' + contentPath);
|
||||||
|
assert(existsSync(thumbPath), 'Thumb file must exist on disk at ' + thumbPath);
|
||||||
|
assert.deepEqual(readFileSync(contentPath), testContent, 'Content on disk must match');
|
||||||
|
assert.deepEqual(readFileSync(thumbPath), testThumb, 'Thumb on disk must match');
|
||||||
|
|
||||||
|
// Verify getById
|
||||||
|
const fetched = await store.manifestattachment.getById(googleAuthId, manifestId, attachment.id);
|
||||||
|
assert.equal(fetched.name, 'test-doc.txt');
|
||||||
|
assert(Buffer.isBuffer(fetched.content), 'Fetched content must be a Buffer');
|
||||||
|
assert.deepEqual(fetched.content, testContent, 'Fetched content must match original');
|
||||||
|
assert(Buffer.isBuffer(fetched.thumbavif), 'Fetched thumbavif must be a Buffer');
|
||||||
|
assert.deepEqual(fetched.thumbavif, testThumb, 'Fetched thumbavif must match original');
|
||||||
|
|
||||||
|
// Verify listByManifestId
|
||||||
|
const list = await store.manifestattachment.listByManifestId(googleAuthId, manifestId);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert(Buffer.isBuffer(list[0].thumbavif), 'List thumbavif must be a Buffer');
|
||||||
|
assert.deepEqual(list[0].thumbavif, testThumb);
|
||||||
|
|
||||||
|
// Verify remove
|
||||||
|
await store.manifestattachment.remove(googleAuthId, manifestId, attachment.id);
|
||||||
|
assert(!existsSync(contentPath), 'Content file should be deleted on remove');
|
||||||
|
assert(!existsSync(thumbPath), 'Thumb file should be deleted on remove');
|
||||||
|
|
||||||
|
// Test delivery creation with inline attached files
|
||||||
|
console.log('1b. Testing delivery.create with inline attached file...');
|
||||||
|
const deliveryWithFile = await store.delivery.create(googleAuthId, {
|
||||||
|
externalNumber: 'DEL-WITH-FILE',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [{ name: 'Item B', amount: 1, net: 5, taxrate: 19 }],
|
||||||
|
files: [{
|
||||||
|
name: 'inline.txt',
|
||||||
|
mimetype: 'text/plain',
|
||||||
|
content: Buffer.from('Inline delivery file content'),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
assert(deliveryWithFile.files && deliveryWithFile.files.length === 1, 'Delivery should have 1 attached file');
|
||||||
|
const inlineHash = deliveryWithFile.files[0].hash;
|
||||||
|
const inlineContentPath = join(storageDir, 'content', inlineHash + '.txt');
|
||||||
|
const inlineThumbPath = join(storageDir, 'thumbavif', inlineHash + '.avif');
|
||||||
|
assert(existsSync(inlineContentPath), 'Inline file content must exist on disk with .txt extension');
|
||||||
|
assert(existsSync(inlineThumbPath), 'Inline file thumb must exist on disk with .avif extension');
|
||||||
|
|
||||||
|
const fetchedDelivery = await store.delivery.getById(googleAuthId, deliveryWithFile.id);
|
||||||
|
assert(fetchedDelivery.files && fetchedDelivery.files.length === 1);
|
||||||
|
assert(fetchedDelivery.files[0].thumbavif, 'Fetched delivery file should include thumbavif');
|
||||||
|
console.log('Manifest attachment tests passed!');
|
||||||
|
|
||||||
|
// 2. Test Article Image (pic table)
|
||||||
|
console.log('2. Testing article.addImage with pic table...');
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Sub Category',
|
||||||
|
});
|
||||||
|
const article = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
name: 'Test Article',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleImage = await sharp({
|
||||||
|
create: {
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 128, b: 255, alpha: 1 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
|
||||||
|
const fullHash = picResult.fullHash;
|
||||||
|
console.log('Article pic added with hash:', fullHash);
|
||||||
|
|
||||||
|
const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif');
|
||||||
|
const picThumbPath = join(storageDir, 'thumbbinary', fullHash + '.avif');
|
||||||
|
assert(existsSync(fullPath), 'Full picture must exist on disk at ' + fullPath);
|
||||||
|
assert(existsSync(picThumbPath), 'Thumb picture must exist on disk at ' + picThumbPath);
|
||||||
|
|
||||||
|
// Verify getImageByHash
|
||||||
|
const fullPic = await store.pic.getImageByHash(googleAuthId, fullHash);
|
||||||
|
assert(Buffer.isBuffer(fullPic.buffer), 'Image buffer must be returned');
|
||||||
|
assert.deepEqual(fullPic.buffer, readFileSync(fullPath));
|
||||||
|
|
||||||
|
// Verify getThumbByHash
|
||||||
|
const thumbPic = await store.pic.getThumbByHash(googleAuthId, fullHash);
|
||||||
|
assert(Buffer.isBuffer(thumbPic.buffer), 'Thumbnail buffer must be returned');
|
||||||
|
assert.deepEqual(thumbPic.buffer, readFileSync(picThumbPath));
|
||||||
|
|
||||||
|
// Verify deleteImage
|
||||||
|
await store.article.deleteImage(googleAuthId, article.sku, fullHash);
|
||||||
|
assert(!existsSync(fullPath), 'Full pic file should be deleted on deleteImage');
|
||||||
|
assert(!existsSync(picThumbPath), 'Thumb pic file should be deleted on deleteImage');
|
||||||
|
console.log('Article pic tests passed!');
|
||||||
|
|
||||||
|
// 3. Test purge
|
||||||
|
console.log('3. Testing purge...');
|
||||||
|
// Add one more file before purge
|
||||||
|
await store.manifestattachment.add(googleAuthId, manifestId, {
|
||||||
|
name: 'purge-test.txt',
|
||||||
|
mimetype: 'text/plain',
|
||||||
|
content: Buffer.from('To be purged'),
|
||||||
|
});
|
||||||
|
await provider.purge();
|
||||||
|
assert(!existsSync(contentPath), 'Files should be purged');
|
||||||
|
console.log('Purge tests passed!');
|
||||||
|
|
||||||
|
console.log('\nAll tests PASSED successfully!');
|
||||||
|
} finally {
|
||||||
|
provider.close();
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests().catch((err) => {
|
||||||
|
console.error('Test failed with error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
220
src/index.js
220
src/index.js
@@ -138,55 +138,52 @@ export default class LibDashStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
// Transitional: store modules still use the raw mssql pool.
|
this.pool = provider.pool;
|
||||||
// They are converted to provider.query() area by area (see TODO.md).
|
|
||||||
const pool = provider.pool;
|
|
||||||
this.pool = pool;
|
|
||||||
|
|
||||||
this.companyuser = {
|
this.companyuser = {
|
||||||
getByEmail: companyuserGetByEmail(pool),
|
getByEmail: companyuserGetByEmail(provider),
|
||||||
add: companyuserAdd(pool),
|
add: companyuserAdd(provider),
|
||||||
delete: companyuserDelete(pool),
|
delete: companyuserDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.company = {
|
this.company = {
|
||||||
create: companyCreate(pool),
|
create: companyCreate(provider),
|
||||||
update: companyUpdate(pool),
|
update: companyUpdate(provider),
|
||||||
delete: companyDelete(pool),
|
delete: companyDelete(provider),
|
||||||
list: companyList(pool),
|
list: companyList(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.category = {
|
this.category = {
|
||||||
getTreeForCompany: categoryGetTreeForCompany(pool),
|
getTreeForCompany: categoryGetTreeForCompany(provider),
|
||||||
getCategoryArticles: categoryGetCategoryArticles(pool),
|
getCategoryArticles: categoryGetCategoryArticles(provider),
|
||||||
createUnderParent: categoryCreateUnderParent(pool),
|
createUnderParent: categoryCreateUnderParent(provider),
|
||||||
ensureByPath: categoryEnsureByPath(pool),
|
ensureByPath: categoryEnsureByPath(provider),
|
||||||
rename: categoryRename(pool),
|
rename: categoryRename(provider),
|
||||||
move: categoryMove(pool),
|
move: categoryMove(provider),
|
||||||
order: categoryOrder(pool),
|
order: categoryOrder(provider),
|
||||||
delete: categoryDelete(pool),
|
delete: categoryDelete(provider),
|
||||||
addImage: categoryAddImage(pool),
|
addImage: categoryAddImage(provider),
|
||||||
deleteImage: categoryDeleteImage(pool),
|
deleteImage: categoryDeleteImage(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.article = {
|
this.article = {
|
||||||
createUnderParent: articleCreateUnderParent(pool),
|
createUnderParent: articleCreateUnderParent(provider),
|
||||||
getBySku: articleGetBySku(pool),
|
getBySku: articleGetBySku(provider),
|
||||||
getHistoryBySku: articleGetHistoryBySku(pool),
|
getHistoryBySku: articleGetHistoryBySku(provider),
|
||||||
listSearchIndex: articleListSearchIndex(pool),
|
listSearchIndex: articleListSearchIndex(provider),
|
||||||
listExport: articleListExport(pool),
|
listExport: articleListExport(provider),
|
||||||
updateBySku: articleUpdateBySku(pool),
|
updateBySku: articleUpdateBySku(provider),
|
||||||
setPricesBySku: articleSetPricesBySku(pool),
|
setPricesBySku: articleSetPricesBySku(provider),
|
||||||
setEansBySku: articleSetEansBySku(pool),
|
setEansBySku: articleSetEansBySku(provider),
|
||||||
setBrandIdBySku: articleSetBrandIdBySku(pool),
|
setBrandIdBySku: articleSetBrandIdBySku(provider),
|
||||||
setWeightBySku: articleSetWeightBySku(pool),
|
setWeightBySku: articleSetWeightBySku(provider),
|
||||||
setCategoryIdsBySku: articleSetCategoryIdsBySku(pool),
|
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
|
||||||
setFatherBySku: articleSetFatherBySku(pool),
|
setFatherBySku: articleSetFatherBySku(provider),
|
||||||
importRows: articleImportRows(pool),
|
importRows: articleImportRows(provider),
|
||||||
addImage: articleAddImage(pool),
|
addImage: articleAddImage(provider),
|
||||||
deleteImage: articleDeleteImage(pool),
|
deleteImage: articleDeleteImage(provider),
|
||||||
orderImages: articleOrderImages(pool),
|
orderImages: articleOrderImages(provider),
|
||||||
delete: articleDelete(pool),
|
delete: articleDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lang = {
|
this.lang = {
|
||||||
@@ -205,105 +202,105 @@ export default class LibDashStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.unit = {
|
this.unit = {
|
||||||
list: unitList(pool),
|
list: unitList(provider),
|
||||||
upsert: unitUpsert(pool),
|
upsert: unitUpsert(provider),
|
||||||
delete: unitDelete(pool),
|
delete: unitDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.companysetting = {
|
this.companysetting = {
|
||||||
list: companysettingList(pool),
|
list: companysettingList(provider),
|
||||||
get: companysettingGet(pool),
|
get: companysettingGet(provider),
|
||||||
set: companysettingSet(pool),
|
set: companysettingSet(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.brand = {
|
this.brand = {
|
||||||
list: brandList(pool),
|
list: brandList(provider),
|
||||||
upsert: brandUpsert(pool),
|
upsert: brandUpsert(provider),
|
||||||
delete: brandDelete(pool),
|
delete: brandDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.customergroup = {
|
this.customergroup = {
|
||||||
list: customergroupList(pool),
|
list: customergroupList(provider),
|
||||||
create: customergroupCreate(pool),
|
create: customergroupCreate(provider),
|
||||||
update: customergroupUpdate(pool),
|
update: customergroupUpdate(provider),
|
||||||
order: customergroupOrder(pool),
|
order: customergroupOrder(provider),
|
||||||
delete: customergroupDelete(pool),
|
delete: customergroupDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.entity = {
|
this.entity = {
|
||||||
list: entityList(pool),
|
list: entityList(provider),
|
||||||
getById: entityGetById(pool),
|
getById: entityGetById(provider),
|
||||||
create: entityCreate(pool),
|
create: entityCreate(provider),
|
||||||
update: entityUpdate(pool),
|
update: entityUpdate(provider),
|
||||||
delete: entityDelete(pool),
|
delete: entityDelete(provider),
|
||||||
peekNextNumber: entityPeekNextNumber(pool),
|
peekNextNumber: entityPeekNextNumber(provider),
|
||||||
isNumberFree: entityIsNumberFree(pool),
|
isNumberFree: entityIsNumberFree(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.entityaddress = {
|
this.entityaddress = {
|
||||||
list: entityaddressList(pool),
|
list: entityaddressList(provider),
|
||||||
create: entityaddressCreate(pool),
|
create: entityaddressCreate(provider),
|
||||||
update: entityaddressUpdate(pool),
|
update: entityaddressUpdate(provider),
|
||||||
delete: entityaddressDelete(pool),
|
delete: entityaddressDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.entitycontact = {
|
this.entitycontact = {
|
||||||
list: entitycontactList(pool),
|
list: entitycontactList(provider),
|
||||||
create: entitycontactCreate(pool),
|
create: entitycontactCreate(provider),
|
||||||
update: entitycontactUpdate(pool),
|
update: entitycontactUpdate(provider),
|
||||||
delete: entitycontactDelete(pool),
|
delete: entitycontactDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.supplier = {
|
this.supplier = {
|
||||||
list: supplierList(pool),
|
list: supplierList(provider),
|
||||||
create: supplierCreate(pool),
|
create: supplierCreate(provider),
|
||||||
update: supplierUpdate(pool),
|
update: supplierUpdate(provider),
|
||||||
delete: supplierDelete(pool),
|
delete: supplierDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.articlesupplier = {
|
this.articlesupplier = {
|
||||||
listByArticleSku: articlesupplierListByArticleSku(pool),
|
listByArticleSku: articlesupplierListByArticleSku(provider),
|
||||||
listByEntityAndSkus: articlesupplierListByEntityAndSkus(pool),
|
listByEntityAndSkus: articlesupplierListByEntityAndSkus(provider),
|
||||||
listBySupplierAndSkus: articlesupplierListByEntityAndSkus(pool),
|
listBySupplierAndSkus: articlesupplierListByEntityAndSkus(provider),
|
||||||
create: articlesupplierCreate(pool),
|
create: articlesupplierCreate(provider),
|
||||||
update: articlesupplierUpdate(pool),
|
update: articlesupplierUpdate(provider),
|
||||||
order: articlesupplierOrder(pool),
|
order: articlesupplierOrder(provider),
|
||||||
delete: articlesupplierDelete(pool),
|
delete: articlesupplierDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.delivery = {
|
this.delivery = {
|
||||||
list: deliveryList(pool),
|
list: deliveryList(provider),
|
||||||
getById: deliveryGetById(pool),
|
getById: deliveryGetById(provider),
|
||||||
create: deliveryCreate(pool),
|
create: deliveryCreate(provider),
|
||||||
update: deliveryUpdate(pool),
|
update: deliveryUpdate(provider),
|
||||||
delete: deliveryDelete(pool),
|
delete: deliveryDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.quote = {
|
this.quote = {
|
||||||
list: quoteList(pool),
|
list: quoteList(provider),
|
||||||
getById: quoteGetById(pool),
|
getById: quoteGetById(provider),
|
||||||
create: quoteCreate(pool),
|
create: quoteCreate(provider),
|
||||||
update: quoteUpdate(pool),
|
update: quoteUpdate(provider),
|
||||||
delete: quoteDelete(pool),
|
delete: quoteDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.order = {
|
this.order = {
|
||||||
list: orderList(pool),
|
list: orderList(provider),
|
||||||
getById: orderGetById(pool),
|
getById: orderGetById(provider),
|
||||||
create: orderCreate(pool),
|
create: orderCreate(provider),
|
||||||
update: orderUpdate(pool),
|
update: orderUpdate(provider),
|
||||||
delete: orderDelete(pool),
|
delete: orderDelete(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.posterminal = {
|
this.posterminal = {
|
||||||
list: posterminalList(pool),
|
list: posterminalList(provider),
|
||||||
upsert: posterminalUpsert(pool),
|
upsert: posterminalUpsert(provider),
|
||||||
delete: posterminalDelete(pool),
|
delete: posterminalDelete(provider),
|
||||||
setOn: posterminalSetOn(pool),
|
setOn: posterminalSetOn(provider),
|
||||||
setOff: posterminalSetOff(pool),
|
setOff: posterminalSetOff(provider),
|
||||||
setOnAll: posterminalSetOnAll(pool),
|
setOnAll: posterminalSetOnAll(provider),
|
||||||
setOffAll: posterminalSetOffAll(pool),
|
setOffAll: posterminalSetOffAll(provider),
|
||||||
getCategoryPosterminalIds: posterminalGetCategoryPosterminalIds(pool),
|
getCategoryPosterminalIds: posterminalGetCategoryPosterminalIds(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.google_auth = {
|
this.google_auth = {
|
||||||
@@ -317,21 +314,21 @@ export default class LibDashStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.pic = {
|
this.pic = {
|
||||||
getImageByHash: picGetImageByHash(pool),
|
getImageByHash: picGetImageByHash(provider),
|
||||||
getThumbByHash: picGetThumbByHash(pool),
|
getThumbByHash: picGetThumbByHash(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.numbers = {
|
this.numbers = {
|
||||||
list: numbersList(pool),
|
list: numbersList(provider),
|
||||||
update: numbersUpdate(pool),
|
update: numbersUpdate(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.manifestattachment = {
|
this.manifestattachment = {
|
||||||
add: manifestattachmentAdd(pool),
|
add: manifestattachmentAdd(provider),
|
||||||
remove: manifestattachmentRemove(pool),
|
remove: manifestattachmentRemove(provider),
|
||||||
listByManifestId: manifestattachmentListByManifestId(pool),
|
listByManifestId: manifestattachmentListByManifestId(provider),
|
||||||
getById: manifestattachmentGetById(pool),
|
getById: manifestattachmentGetById(provider),
|
||||||
order: manifestattachmentOrder(pool),
|
order: manifestattachmentOrder(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.purge = storePurge(provider);
|
this.purge = storePurge(provider);
|
||||||
@@ -347,3 +344,6 @@ export default class LibDashStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { SCHEMA_SCRIPTS } from './providers/mssql/schema.js';
|
export { SCHEMA_SCRIPTS } from './providers/mssql/schema.js';
|
||||||
|
export { BetterSqlite3Provider } from './providers/sqlite3/index.js';
|
||||||
|
export { MssqlProvider } from './providers/mssql/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -133,8 +133,12 @@ export class MssqlProvider {
|
|||||||
const tx = pool.transaction();
|
const tx = pool.transaction();
|
||||||
await tx.begin();
|
await tx.begin();
|
||||||
try {
|
try {
|
||||||
const runner = this.makeExecutor(() => tx.request());
|
const executor = this.makeExecutor(() => tx.request());
|
||||||
const result = await fn(runner);
|
const runner = async (sql, params) => executor.query(sql, params);
|
||||||
|
runner.query = runner;
|
||||||
|
runner.exec = (proc, opts) => executor.exec(proc, opts);
|
||||||
|
runner.dialect = 'mssql';
|
||||||
|
const result = await fn(runner, tx);
|
||||||
await tx.commit();
|
await tx.commit();
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -148,17 +152,32 @@ export class MssqlProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Implemented by store/initialize.js — no-op here (keeps parity with
|
const pool = await this.ensurePool();
|
||||||
// other providers, which may run migrations internally instead).
|
const { SCHEMA_SCRIPTS } = await import('./schema.js');
|
||||||
|
for (const script of SCHEMA_SCRIPTS) {
|
||||||
|
if (script && script.trim()) {
|
||||||
|
await pool.request().query(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async purge() {
|
async purge() {
|
||||||
// Implemented by store/purge.js — no-op here.
|
const pool = await this.ensurePool();
|
||||||
|
const { buildPurgeSql } = await import('./purgeSql.js');
|
||||||
|
await pool.request().query(buildPurgeSql());
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
return this.pool ? this.pool.close() : Promise.resolve();
|
return this.pool ? this.pool.close() : Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUniqueConstraintError(err) {
|
||||||
|
return isUniqueConstraintError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
isForeignKeyConstraintError(err) {
|
||||||
|
return isForeignKeyConstraintError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** mssql: 2627 = PK/unique violation, 2601 = unique index violation */
|
/** mssql: 2627 = PK/unique violation, 2601 = unique index violation */
|
||||||
|
|||||||
@@ -44,3 +44,14 @@ OUTPUT INSERTED.id, INSERTED.[type], INSERTED.prefix, INSERTED.padding, INSERTED
|
|||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
AND company_id = @companyId
|
AND company_id = @companyId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_FOR_UPDATE = `
|
||||||
|
SELECT id, [type], prefix, padding, [next] AS [next]
|
||||||
|
FROM numbers WITH (UPDLOCK, ROWLOCK)
|
||||||
|
WHERE company_id = @companyId AND [type] = @type;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INCREMENT = `
|
||||||
|
UPDATE numbers SET [next] = [next] + 1 WHERE id = @id;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,26 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an error is a unique-constraint violation.
|
* Checks whether an error is a unique-constraint violation.
|
||||||
* @param {unknown} err
|
* @param {any} err
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isUniqueConstraintError(err) {
|
export function isUniqueConstraintError(err) {
|
||||||
throw new Error('Not implemented: isUniqueConstraintError');
|
return (
|
||||||
|
err?.number === 2627 ||
|
||||||
|
err?.number === 2601 ||
|
||||||
|
err?.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
|
||||||
|
err?.code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an error is a foreign-key-constraint violation.
|
* Checks whether an error is a foreign-key-constraint violation.
|
||||||
* @param {unknown} err
|
* @param {any} err
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isForeignKeyConstraintError(err) {
|
export function isForeignKeyConstraintError(err) {
|
||||||
throw new Error('Not implemented: isForeignKeyConstraintError');
|
return (
|
||||||
|
err?.number === 547 ||
|
||||||
|
err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* Provider for a caller-owned `better-sqlite3` Database instance.
|
* Provider for `better-sqlite3` Database instances or connection options.
|
||||||
*
|
*
|
||||||
* The driver is synchronous, but this adapter exposes the asynchronous
|
* The driver is synchronous, but this adapter exposes the asynchronous
|
||||||
* provider contract used by LibDashStore.
|
* provider contract used by LibDashStore.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { SCHEMA_SCRIPTS } from './schema.js';
|
||||||
|
|
||||||
|
function normalizeParamValue(value) {
|
||||||
|
if (value === undefined || value === null) return null;
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return isNaN(value.getTime()) ? null : value.toISOString();
|
||||||
|
}
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function bindParams(params) {
|
function bindParams(params) {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(params).map(([key, value]) => [
|
Object.entries(params).map(([key, value]) => [
|
||||||
key.replace(/^[@:$]/, ''),
|
key.replace(/^[@:$]/, ''),
|
||||||
value === undefined ? null : value,
|
normalizeParamValue(value),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -20,10 +34,169 @@ function normalizeInsertId(value) {
|
|||||||
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { mkdirSync, mkdtempSync, promises as fsPromises } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
|
||||||
|
export function extFromMimetype(mimetype) {
|
||||||
|
if (!mimetype) return '';
|
||||||
|
const type = mimetype.trim().toLowerCase().split(';')[0];
|
||||||
|
const map = {
|
||||||
|
'application/pdf': '.pdf',
|
||||||
|
'text/plain': '.txt',
|
||||||
|
'text/csv': '.csv',
|
||||||
|
'text/html': '.html',
|
||||||
|
'text/markdown': '.md',
|
||||||
|
'image/avif': '.avif',
|
||||||
|
'image/png': '.png',
|
||||||
|
'image/jpeg': '.jpg',
|
||||||
|
'image/jpg': '.jpg',
|
||||||
|
'image/webp': '.webp',
|
||||||
|
'image/gif': '.gif',
|
||||||
|
'image/svg+xml': '.svg',
|
||||||
|
'application/json': '.json',
|
||||||
|
'application/zip': '.zip',
|
||||||
|
'application/xml': '.xml',
|
||||||
|
'application/octet-stream': '.bin',
|
||||||
|
};
|
||||||
|
if (map[type]) return map[type];
|
||||||
|
if (type.startsWith('image/')) return '.' + type.slice(6).replace('+xml', '');
|
||||||
|
if (type.startsWith('text/')) return '.' + type.slice(5);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeWrite(filePath, buffer) {
|
||||||
|
if (buffer == null) return;
|
||||||
|
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
||||||
|
await fsPromises.writeFile(filePath, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findFileByHash(dir, hash, ext = '') {
|
||||||
|
if (!dir || !hash) return null;
|
||||||
|
if (ext) {
|
||||||
|
const candidate = join(dir, hash + ext);
|
||||||
|
try {
|
||||||
|
await fsPromises.access(candidate);
|
||||||
|
return candidate;
|
||||||
|
} catch {
|
||||||
|
// fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const candidateNoExt = join(dir, hash);
|
||||||
|
try {
|
||||||
|
await fsPromises.access(candidateNoExt);
|
||||||
|
return candidateNoExt;
|
||||||
|
} catch {
|
||||||
|
// fallback
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const files = await fsPromises.readdir(dir);
|
||||||
|
const match = files.find((f) => f === hash || f.startsWith(hash + '.'));
|
||||||
|
if (match) return join(dir, match);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeReadByHash(dir, hash, ext = '') {
|
||||||
|
const filePath = await findFileByHash(dir, hash, ext);
|
||||||
|
if (!filePath) return null;
|
||||||
|
try {
|
||||||
|
return await fsPromises.readFile(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function safeDeleteByHash(dir, hash) {
|
||||||
|
if (!dir || !hash) return;
|
||||||
|
try {
|
||||||
|
const files = await fsPromises.readdir(dir);
|
||||||
|
const matches = files.filter((f) => f === hash || f.startsWith(hash + '.'));
|
||||||
|
await Promise.all(matches.map((f) => fsPromises.unlink(join(dir, f))));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BetterSqlite3Executor {
|
export class BetterSqlite3Executor {
|
||||||
/** @param {import('better-sqlite3').Database} db */
|
/**
|
||||||
constructor(db) {
|
* @param {import('better-sqlite3').Database} db
|
||||||
|
* @param {string|null} [storageDir=null]
|
||||||
|
*/
|
||||||
|
constructor(db, storageDir = null) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.dialect = 'sqlite3';
|
||||||
|
this.storageDir = storageDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFileContent(hash, buffer, mimetype = null) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
const ext = extFromMimetype(mimetype);
|
||||||
|
await safeWrite(join(this.storageDir, 'content', hash + ext), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFileContent(hash, mimetype = null) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
const ext = extFromMimetype(mimetype);
|
||||||
|
return safeReadByHash(join(this.storageDir, 'content'), hash, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeThumbAvif(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'thumbavif', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readThumbAvif(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'thumbavif'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writePicFull(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'fullbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicFull(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'fullbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async writePicThumb(hash, buffer) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeWrite(join(this.storageDir, 'thumbbinary', hash + '.avif'), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readPicThumb(hash) {
|
||||||
|
if (!this.storageDir || !hash) return null;
|
||||||
|
return safeReadByHash(join(this.storageDir, 'thumbbinary'), hash, '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(hash) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'content'), hash);
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'thumbavif'), hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePic(hash) {
|
||||||
|
if (!this.storageDir || !hash) return;
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
|
||||||
|
await safeDeleteByHash(join(this.storageDir, 'thumbbinary'), hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
async purgeFiles() {
|
||||||
|
if (!this.storageDir) return;
|
||||||
|
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) {
|
||||||
|
const dir = join(this.storageDir, sub);
|
||||||
|
try {
|
||||||
|
const files = await fsPromises.readdir(dir);
|
||||||
|
await Promise.all(files.map((f) => fsPromises.unlink(join(dir, f))));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
/** @returns {Promise<import('../provider.js').QueryResult>} */
|
||||||
@@ -49,11 +222,47 @@ export class BetterSqlite3Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureDirectory(filename) {
|
||||||
|
if (typeof filename === 'string' && filename !== ':memory:' && !filename.startsWith(':memory:')) {
|
||||||
|
const dir = dirname(filename);
|
||||||
|
if (dir && dir !== '.') {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
||||||
/**
|
/**
|
||||||
* @param {import('better-sqlite3').Database} db An open, caller-owned database.
|
* @param {import('better-sqlite3').Database | string | { filename: string, storageDir?: string, filesDir?: string, [key: string]: any }} [dbOrConfig]
|
||||||
|
* An open better-sqlite3 Database, a file path (or ':memory:'), or a config object.
|
||||||
*/
|
*/
|
||||||
constructor(db) {
|
constructor(dbOrConfig = ':memory:') {
|
||||||
|
let db = dbOrConfig;
|
||||||
|
let storageDir = null;
|
||||||
|
|
||||||
|
if (typeof dbOrConfig === 'string') {
|
||||||
|
ensureDirectory(dbOrConfig);
|
||||||
|
db = new Database(dbOrConfig);
|
||||||
|
storageDir = (dbOrConfig === ':memory:' || dbOrConfig.startsWith(':memory:'))
|
||||||
|
? mkdtempSync(join(tmpdir(), 'dash-sqlite-storage-'))
|
||||||
|
: join(dirname(dbOrConfig), 'storage');
|
||||||
|
} else if (
|
||||||
|
dbOrConfig &&
|
||||||
|
typeof dbOrConfig === 'object' &&
|
||||||
|
typeof dbOrConfig.prepare !== 'function'
|
||||||
|
) {
|
||||||
|
const { filename, storageDir: customStorageDir, filesDir, ...options } = dbOrConfig;
|
||||||
|
if (filename) ensureDirectory(filename);
|
||||||
|
db = new Database(filename || ':memory:', options);
|
||||||
|
storageDir = customStorageDir || filesDir || (
|
||||||
|
(!filename || filename === ':memory:' || filename.startsWith(':memory:'))
|
||||||
|
? mkdtempSync(join(tmpdir(), 'dash-sqlite-storage-'))
|
||||||
|
: join(dirname(filename), 'storage')
|
||||||
|
);
|
||||||
|
} else if (db && typeof db.prepare === 'function') {
|
||||||
|
storageDir = mkdtempSync(join(tmpdir(), 'dash-sqlite-storage-'));
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!db ||
|
!db ||
|
||||||
typeof db.prepare !== 'function' ||
|
typeof db.prepare !== 'function' ||
|
||||||
@@ -61,22 +270,29 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
typeof db.pragma !== 'function'
|
typeof db.pragma !== 'function'
|
||||||
) {
|
) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'BetterSqlite3Provider requires an open better-sqlite3 Database',
|
'BetterSqlite3Provider requires an open better-sqlite3 Database or a filename string',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
super(db);
|
super(db, storageDir);
|
||||||
this.dialect = 'sqlite3';
|
if (this.storageDir) {
|
||||||
|
for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) {
|
||||||
|
mkdirSync(join(this.storageDir, sub), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pool = this;
|
||||||
db.pragma('foreign_keys = ON');
|
db.pragma('foreign_keys = ON');
|
||||||
db.pragma('journal_mode = WAL');
|
db.pragma('journal_mode = WAL');
|
||||||
db.pragma('busy_timeout = 5000');
|
db.pragma('busy_timeout = 5000');
|
||||||
}
|
}
|
||||||
|
|
||||||
#transactionTail = Promise.resolve();
|
#transactionTail = Promise.resolve();
|
||||||
|
#transactionDepth = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs `fn(runner)` in an immediate write transaction. Transactions are
|
* Runs `fn(runner)` in an immediate write transaction. Transactions are
|
||||||
* serialized because better-sqlite3 has one synchronous connection.
|
* serialized because better-sqlite3 has one synchronous connection.
|
||||||
|
* Nested calls use SAVEPOINTs.
|
||||||
*
|
*
|
||||||
* @template T
|
* @template T
|
||||||
* @param {(runner: BetterSqlite3Executor) => Promise<T> | T} fn
|
* @param {(runner: BetterSqlite3Executor) => Promise<T> | T} fn
|
||||||
@@ -87,15 +303,66 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
return Promise.reject(new TypeError('transaction requires a callback'));
|
return Promise.reject(new TypeError('transaction requires a callback'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bindRunner = (executor) => {
|
||||||
|
const runner = async (sql, params) => executor.query(sql, params);
|
||||||
|
runner.query = runner;
|
||||||
|
runner.dialect = 'sqlite3';
|
||||||
|
runner.storageDir = this.storageDir;
|
||||||
|
runner.writeFileContent = executor.writeFileContent.bind(executor);
|
||||||
|
runner.readFileContent = executor.readFileContent.bind(executor);
|
||||||
|
runner.writeThumbAvif = executor.writeThumbAvif.bind(executor);
|
||||||
|
runner.readThumbAvif = executor.readThumbAvif.bind(executor);
|
||||||
|
runner.writePicFull = executor.writePicFull.bind(executor);
|
||||||
|
runner.readPicFull = executor.readPicFull.bind(executor);
|
||||||
|
runner.writePicThumb = executor.writePicThumb.bind(executor);
|
||||||
|
runner.readPicThumb = executor.readPicThumb.bind(executor);
|
||||||
|
runner.deleteFile = executor.deleteFile.bind(executor);
|
||||||
|
runner.deletePic = executor.deletePic.bind(executor);
|
||||||
|
runner.purgeFiles = executor.purgeFiles.bind(executor);
|
||||||
|
return runner;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.#transactionDepth > 0 || this.db.inTransaction) {
|
||||||
|
const savepoint = `sp_${++this.#transactionDepth}`;
|
||||||
|
this.db.exec(`SAVEPOINT ${savepoint}`);
|
||||||
|
const executor = new BetterSqlite3Executor(this.db, this.storageDir);
|
||||||
|
const runner = bindRunner(executor);
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => fn(runner, executor))
|
||||||
|
.then(
|
||||||
|
(result) => {
|
||||||
|
this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
||||||
|
this.#transactionDepth--;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
try {
|
||||||
|
this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
||||||
|
this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
this.#transactionDepth--;
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
|
this.#transactionDepth = 1;
|
||||||
this.db.exec('BEGIN IMMEDIATE');
|
this.db.exec('BEGIN IMMEDIATE');
|
||||||
try {
|
try {
|
||||||
const result = await fn(new BetterSqlite3Executor(this.db));
|
const executor = new BetterSqlite3Executor(this.db, this.storageDir);
|
||||||
|
const runner = bindRunner(executor);
|
||||||
|
const result = await fn(runner, executor);
|
||||||
this.db.exec('COMMIT');
|
this.db.exec('COMMIT');
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this.db.inTransaction) this.db.exec('ROLLBACK');
|
if (this.db.inTransaction) this.db.exec('ROLLBACK');
|
||||||
throw err;
|
throw err;
|
||||||
|
} finally {
|
||||||
|
this.#transactionDepth = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,18 +375,67 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
throw new Error('Stored procedures are not supported by better-sqlite3');
|
throw new Error('Stored procedures are not supported by better-sqlite3');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the native SQLite schema.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// SQLite migrations are provided by the SQLite schema layer.
|
for (const script of SCHEMA_SCRIPTS) {
|
||||||
|
if (script && script.trim()) {
|
||||||
|
this.db.exec(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purges all tables, views, and triggers from the SQLite database.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async purge() {
|
async purge() {
|
||||||
// SQLite schema teardown is provided by the SQLite schema layer.
|
await this.purgeFiles();
|
||||||
|
this.db.pragma('foreign_keys = OFF');
|
||||||
|
try {
|
||||||
|
const tables = this.db
|
||||||
|
.prepare(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.map((r) => r.name);
|
||||||
|
const views = this.db
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'view'")
|
||||||
|
.all()
|
||||||
|
.map((r) => r.name);
|
||||||
|
const triggers = this.db
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger'")
|
||||||
|
.all()
|
||||||
|
.map((r) => r.name);
|
||||||
|
|
||||||
|
for (const trigger of triggers) {
|
||||||
|
this.db.exec(`DROP TRIGGER IF EXISTS "${trigger}"`);
|
||||||
|
}
|
||||||
|
for (const view of views) {
|
||||||
|
this.db.exec(`DROP VIEW IF EXISTS "${view}"`);
|
||||||
|
}
|
||||||
|
for (const table of tables) {
|
||||||
|
this.db.exec(`DROP TABLE IF EXISTS "${table}"`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.db.pragma('foreign_keys = ON');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Explicitly closes the caller-owned database. It is never closed implicitly. */
|
/** Explicitly closes the database. */
|
||||||
close() {
|
close() {
|
||||||
this.db.close();
|
this.db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUniqueConstraintError(err) {
|
||||||
|
return isUniqueConstraintError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
isForeignKeyConstraintError(err) {
|
||||||
|
return isForeignKeyConstraintError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUniqueConstraintError(err) {
|
export function isUniqueConstraintError(err) {
|
||||||
@@ -131,4 +447,7 @@ export function isUniqueConstraintError(err) {
|
|||||||
|
|
||||||
export function isForeignKeyConstraintError(err) {
|
export function isForeignKeyConstraintError(err) {
|
||||||
return err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY';
|
return err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { BetterSqlite3Provider as SqliteProvider };
|
||||||
|
export { BetterSqlite3Executor as SqliteExecutor };
|
||||||
@@ -5,10 +5,60 @@ import { ARTICLE_LIST_COLUMNS, ARTICLE_LIST_JOINS } from './article.js';
|
|||||||
const queries = translateQueries(source, 'category');
|
const queries = translateQueries(source, 'category');
|
||||||
export const {
|
export const {
|
||||||
CREATE, DELETE, GET_BY_PARENT_AND_NAME,
|
CREATE, DELETE, GET_BY_PARENT_AND_NAME,
|
||||||
GET_NEXT_SIBLING_SORT, IS_ANCESTOR, LIST_CHILD_IDS,
|
GET_NEXT_SIBLING_SORT, LIST_CHILD_IDS,
|
||||||
MOVE, RESOLVE_CATEGORY_ACCESS, UPDATE_SORT,
|
MOVE, UPDATE_SORT,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
export const RESOLVE_CATEGORY_ACCESS = `
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT id, parent_id
|
||||||
|
FROM category
|
||||||
|
WHERE id = @categoryId
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT c.id, c.parent_id
|
||||||
|
FROM category c
|
||||||
|
INNER JOIN ancestors a ON c.id = a.parent_id
|
||||||
|
),
|
||||||
|
root AS (
|
||||||
|
SELECT id AS rootCategoryId
|
||||||
|
FROM ancestors
|
||||||
|
WHERE parent_id IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.id AS categoryId,
|
||||||
|
r.rootCategoryId,
|
||||||
|
co.id AS companyId,
|
||||||
|
co.default_language_id AS languageId,
|
||||||
|
co.poscustomergroup_id AS posCustomerGroupId,
|
||||||
|
cu.id AS companyuserId
|
||||||
|
FROM category p
|
||||||
|
LEFT JOIN root r ON 1 = 1
|
||||||
|
LEFT JOIN company co ON co.root_category_id = r.rootCategoryId
|
||||||
|
LEFT JOIN companyuser cu
|
||||||
|
ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId
|
||||||
|
WHERE p.id = @categoryId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IS_ANCESTOR = `
|
||||||
|
WITH RECURSIVE ancestors AS (
|
||||||
|
SELECT id, parent_id
|
||||||
|
FROM category
|
||||||
|
WHERE id = @parentId
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT c.id, c.parent_id
|
||||||
|
FROM category c
|
||||||
|
INNER JOIN ancestors a ON c.id = a.parent_id
|
||||||
|
)
|
||||||
|
SELECT id
|
||||||
|
FROM ancestors
|
||||||
|
WHERE id = @id
|
||||||
|
`;
|
||||||
|
|
||||||
export const GET_CATEGORY_ARTICLES = `
|
export const GET_CATEGORY_ARTICLES = `
|
||||||
SELECT ${ARTICLE_LIST_COLUMNS}
|
SELECT ${ARTICLE_LIST_COLUMNS}
|
||||||
FROM categoryarticle ca
|
FROM categoryarticle ca
|
||||||
|
|||||||
@@ -5,18 +5,41 @@ const queries = translateQueries(source, 'delivery');
|
|||||||
export const {
|
export const {
|
||||||
CREATE_ATTACHMENT, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
|
CREATE_ATTACHMENT, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
|
||||||
DELETE_MANIFEST, DELIVERY_TYPE, GET_DELIVERY_HEADER, GET_DELIVERY_LINES,
|
DELETE_MANIFEST, DELIVERY_TYPE, GET_DELIVERY_HEADER, GET_DELIVERY_LINES,
|
||||||
GET_NEXT_ATTACHMENT_SORT, LIST_DELIVERIES, LIST_DELIVERY_FILES,
|
GET_NEXT_ATTACHMENT_SORT, LIST_DELIVERIES,
|
||||||
UPDATE_MANIFEST,
|
UPDATE_MANIFEST,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
export const CREATE_FILE = `
|
export const CREATE_FILE = `
|
||||||
INSERT INTO "file" (
|
INSERT INTO "file" (
|
||||||
hash, mimetype, name, size, created, modified, content,
|
hash, mimetype, name, size, created, modified,
|
||||||
thumbavif, thumbx, thumby, meta
|
thumbx, thumby, meta
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@hash, @mimetype, @name, @size, @created, @modified, @content,
|
@hash, @mimetype, @name, @size, @created, @modified,
|
||||||
@thumbavif, @thumbx, @thumby, @meta
|
@thumbx, @thumby, @meta
|
||||||
)
|
)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_DELIVERY_FILES = `
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.manifest_id AS manifestId,
|
||||||
|
ma.file_id AS fileId,
|
||||||
|
ma.sort,
|
||||||
|
f.hash,
|
||||||
|
f.mimetype,
|
||||||
|
f.name,
|
||||||
|
f.size,
|
||||||
|
f.created,
|
||||||
|
f.modified,
|
||||||
|
f.thumbx,
|
||||||
|
f.thumby,
|
||||||
|
f.meta
|
||||||
|
FROM manifestattachment ma
|
||||||
|
INNER JOIN "file" f ON f.id = ma.file_id
|
||||||
|
INNER JOIN manifest m ON m.id = ma.manifest_id
|
||||||
|
WHERE m.company_id = @companyId
|
||||||
|
AND m."type" = @type
|
||||||
|
ORDER BY ma.manifest_id, ma.sort ASC, ma.id ASC
|
||||||
`;
|
`;
|
||||||
@@ -4,18 +4,68 @@ import { translateQueries } from './translate.js';
|
|||||||
const queries = translateQueries(source, 'manifestattachment');
|
const queries = translateQueries(source, 'manifestattachment');
|
||||||
export const {
|
export const {
|
||||||
CHECK_MANIFEST_ACCESS, CREATE_ATTACHMENT, DELETE_ATTACHMENT,
|
CHECK_MANIFEST_ACCESS, CREATE_ATTACHMENT, DELETE_ATTACHMENT,
|
||||||
DELETE_FILE, FIND_ATTACHMENT, GET_BY_ID, GET_NEXT_SORT, LIST_BY_MANIFEST,
|
DELETE_FILE, GET_NEXT_SORT,
|
||||||
LIST_IDS_BY_MANIFEST, SET_TEMP_SORTS, UPDATE_SORT,
|
LIST_IDS_BY_MANIFEST, SET_TEMP_SORTS, UPDATE_SORT,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
export const FIND_ATTACHMENT = `
|
||||||
|
SELECT ma.id, ma.file_id AS fileId, f.hash
|
||||||
|
FROM manifestattachment ma
|
||||||
|
INNER JOIN "file" f ON f.id = ma.file_id
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
AND (ma.id = @id OR ma.file_id = @id)
|
||||||
|
`;
|
||||||
|
|
||||||
export const CREATE_FILE = `
|
export const CREATE_FILE = `
|
||||||
INSERT INTO "file" (
|
INSERT INTO "file" (
|
||||||
hash, mimetype, name, size, created, modified, content,
|
hash, mimetype, name, size, created, modified,
|
||||||
thumbavif, thumbx, thumby, meta
|
thumbx, thumby, meta
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@hash, @mimetype, @name, @size, @created, @modified, @content,
|
@hash, @mimetype, @name, @size, @created, @modified,
|
||||||
@thumbavif, @thumbx, @thumby, @meta
|
@thumbx, @thumby, @meta
|
||||||
)
|
)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_BY_MANIFEST = `
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.manifest_id AS manifestId,
|
||||||
|
ma.file_id AS fileId,
|
||||||
|
ma.sort,
|
||||||
|
f.hash,
|
||||||
|
f.mimetype,
|
||||||
|
f.name,
|
||||||
|
f.size,
|
||||||
|
f.created,
|
||||||
|
f.modified,
|
||||||
|
f.thumbx,
|
||||||
|
f.thumby,
|
||||||
|
f.meta
|
||||||
|
FROM manifestattachment ma
|
||||||
|
INNER JOIN "file" f ON f.id = ma.file_id
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
ORDER BY ma.sort ASC, ma.id ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_BY_ID = `
|
||||||
|
SELECT
|
||||||
|
ma.id,
|
||||||
|
ma.manifest_id AS manifestId,
|
||||||
|
ma.file_id AS fileId,
|
||||||
|
ma.sort,
|
||||||
|
f.hash,
|
||||||
|
f.mimetype,
|
||||||
|
f.name,
|
||||||
|
f.size,
|
||||||
|
f.created,
|
||||||
|
f.modified,
|
||||||
|
f.thumbx,
|
||||||
|
f.thumby,
|
||||||
|
f.meta
|
||||||
|
FROM manifestattachment ma
|
||||||
|
INNER JOIN "file" f ON f.id = ma.file_id
|
||||||
|
WHERE ma.manifest_id = @manifestId
|
||||||
|
AND (ma.id = @id OR ma.file_id = @id)
|
||||||
`;
|
`;
|
||||||
@@ -3,9 +3,31 @@ import { translateQueries } from './translate.js';
|
|||||||
|
|
||||||
const queries = translateQueries(source, 'pic');
|
const queries = translateQueries(source, 'pic');
|
||||||
export const {
|
export const {
|
||||||
CREATE, GET_CATEGORY_ID_BY_HASH, GET_FULL_BY_HASH, GET_THUMB_BY_HASH,
|
GET_CATEGORY_ID_BY_HASH,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
export const CREATE = `
|
||||||
|
INSERT INTO pic (
|
||||||
|
w, h, size, hash, thumbw, thumbh, thumbhash, source
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_FULL_BY_HASH = `
|
||||||
|
SELECT w, h, size, hash
|
||||||
|
FROM pic
|
||||||
|
WHERE hash = @hash
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_THUMB_BY_HASH = `
|
||||||
|
SELECT thumbw AS w, thumbh AS h, hash
|
||||||
|
FROM pic
|
||||||
|
WHERE hash = @hash
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_BY_ARTICLE_AND_HASH = `
|
export const DELETE_BY_ARTICLE_AND_HASH = `
|
||||||
DELETE FROM pic
|
DELETE FROM pic
|
||||||
WHERE hash = @fullHash
|
WHERE hash = @fullHash
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ CREATE TABLE IF NOT EXISTS pic (
|
|||||||
thumbw INTEGER NOT NULL,
|
thumbw INTEGER NOT NULL,
|
||||||
thumbh INTEGER NOT NULL,
|
thumbh INTEGER NOT NULL,
|
||||||
thumbhash TEXT NOT NULL,
|
thumbhash TEXT NOT NULL,
|
||||||
source TEXT NOT NULL,
|
source TEXT NOT NULL
|
||||||
fullbinary BLOB NOT NULL,
|
|
||||||
thumbbinary BLOB NOT NULL
|
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -542,8 +540,6 @@ CREATE TABLE IF NOT EXISTS "file" (
|
|||||||
size INTEGER NOT NULL,
|
size INTEGER NOT NULL,
|
||||||
created TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
created TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
modified TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
modified TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
content BLOB NOT NULL,
|
|
||||||
thumbavif BLOB,
|
|
||||||
thumbx INTEGER,
|
thumbx INTEGER,
|
||||||
thumby INTEGER,
|
thumby INTEGER,
|
||||||
meta TEXT
|
meta TEXT
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import { ADD as ADD_ARTICLEPIC, GET_NEXT_SORT } from '../../providers/mssql/queries/articlepic.js';
|
import {
|
||||||
import { CREATE as CREATE_PIC } from '../../providers/mssql/queries/pic.js';
|
ADD as MSSQL_ADD_ARTICLEPIC,
|
||||||
|
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/mssql/queries/articlepic.js';
|
||||||
|
import {
|
||||||
|
ADD as SQLITE_ADD_ARTICLEPIC,
|
||||||
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/articlepic.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.js';
|
||||||
import processAvifImage from '../../util/processAvifImage.js';
|
import processAvifImage from '../../util/processAvifImage.js';
|
||||||
import newFullHash from '../../util/newFullHash.js';
|
import newFullHash from '../../util/newFullHash.js';
|
||||||
import request from '../request.js';
|
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function addImage(pool) {
|
export default function addImage(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
|
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
|
||||||
|
const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an image to an article by its SKU, processing it into AVIF variants.
|
* Adds an image to an article by its SKU, processing it into AVIF variants.
|
||||||
@@ -14,50 +27,53 @@ export default function addImage(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {Buffer} image - The source image buffer.
|
* @param {Buffer} image - The source image buffer.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
|
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
|
* const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
|
||||||
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, image) => {
|
return async (googleAuthId, sku, image, tx) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const runAdd = async (runner) => {
|
||||||
|
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const processed = await processAvifImage(image);
|
const processed = await processAvifImage(image);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const sortResult = await runner.query(getSortSql, { articleId });
|
||||||
await tx.begin();
|
const sort = sortResult.rows[0].sort;
|
||||||
|
|
||||||
try {
|
|
||||||
const sortResult = await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(GET_NEXT_SORT);
|
|
||||||
|
|
||||||
const sort = sortResult.recordset[0].sort;
|
|
||||||
const fullHash = newFullHash();
|
const fullHash = newFullHash();
|
||||||
|
|
||||||
const picResult = await request(pool, tx)
|
if (isSqlite) {
|
||||||
.input('w', processed.full.w)
|
if (runner.writePicFull) {
|
||||||
.input('h', processed.full.h)
|
await runner.writePicFull(fullHash, processed.full.buffer);
|
||||||
.input('size', processed.full.size)
|
await runner.writePicThumb(fullHash, processed.thumb.buffer);
|
||||||
.input('hash', fullHash)
|
} else if (provider?.writePicFull) {
|
||||||
.input('thumbw', processed.thumb.w)
|
await provider.writePicFull(fullHash, processed.full.buffer);
|
||||||
.input('thumbh', processed.thumb.h)
|
await provider.writePicThumb(fullHash, processed.thumb.buffer);
|
||||||
.input('thumbhash', processed.thumb.hash)
|
}
|
||||||
.input('source', '')
|
}
|
||||||
.input('fullbinary', processed.full.buffer)
|
|
||||||
.input('thumbbinary', processed.thumb.buffer)
|
|
||||||
.query(CREATE_PIC);
|
|
||||||
|
|
||||||
const picId = picResult.recordset[0].id;
|
const picResult = await runner.query(createPicSql, {
|
||||||
|
w: processed.full.w,
|
||||||
|
h: processed.full.h,
|
||||||
|
size: processed.full.size,
|
||||||
|
hash: fullHash,
|
||||||
|
thumbw: processed.thumb.w,
|
||||||
|
thumbh: processed.thumb.h,
|
||||||
|
thumbhash: processed.thumb.hash,
|
||||||
|
source: '',
|
||||||
|
fullbinary: processed.full.buffer,
|
||||||
|
thumbbinary: processed.thumb.buffer,
|
||||||
|
});
|
||||||
|
|
||||||
await request(pool, tx)
|
const picId = picResult.rows[0]?.id ?? picResult.insertId;
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('picId', picId)
|
|
||||||
.input('sort', sort)
|
|
||||||
.query(ADD_ARTICLEPIC);
|
|
||||||
|
|
||||||
await tx.commit();
|
await runner.query(addArticlePicSql, {
|
||||||
|
articleId,
|
||||||
|
picId,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
picId,
|
picId,
|
||||||
@@ -74,13 +90,15 @@ export default function addImage(pool) {
|
|||||||
fullHash,
|
fullHash,
|
||||||
thumbHash: processed.thumb.hash,
|
thumbHash: processed.thumb.hash,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runAdd(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runAdd);
|
||||||
|
}
|
||||||
|
return runAdd(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import { CREATE } from '../../providers/mssql/queries/article.js';
|
import { CREATE as MSSQL_CREATE_ARTICLE } from '../../providers/mssql/queries/article.js';
|
||||||
import { ADD as ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
import { CREATE as SQLITE_CREATE_ARTICLE } from '../../providers/sqlite3/queries/article.js';
|
||||||
import { CREATE as CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
||||||
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||||
import nextSku from '../numbers/nextSku.js';
|
import nextSku from '../numbers/nextSku.js';
|
||||||
import request from '../request.js';
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
|
||||||
export default function createUnderParent(pool) {
|
export default function createUnderParent(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
const ensureUnit = ensureDefaultUnit(pool);
|
const ensureUnit = ensureDefaultUnit(provider);
|
||||||
const allocateSku = nextSku(pool);
|
const allocateSku = nextSku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
||||||
|
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||||
|
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new article under a given parent category, allocating a SKU and
|
* Creates a new article under a given parent category, allocating a SKU and
|
||||||
@@ -21,54 +31,51 @@ export default function createUnderParent(pool) {
|
|||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.parentId - The id of the parent category.
|
* @param {number} input.parentId - The id of the parent category.
|
||||||
* @param {string} input.name - The article name.
|
* @param {string} input.name - The article name.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article.
|
* @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' });
|
* const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' });
|
||||||
* // => { id, parentId, name, sku }
|
* // => { id, parentId, name, sku }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, parentId, name }) => {
|
return async ({ googleAuthId, parentId, name }, tx) => {
|
||||||
const { companyId, languageId, companyuserId } = await access(
|
const runCreate = async (runner) => {
|
||||||
googleAuthId,
|
const { companyId, languageId, companyuserId } = await access(
|
||||||
parentId
|
googleAuthId,
|
||||||
);
|
parentId,
|
||||||
|
runner,
|
||||||
|
);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const unitId = await ensureUnit(runner, languageId, companyId);
|
||||||
await tx.begin();
|
const sku = await allocateSku(companyId, runner);
|
||||||
|
|
||||||
try {
|
|
||||||
const unitId = await ensureUnit(tx, languageId, companyId);
|
|
||||||
const sku = await allocateSku(companyId, tx);
|
|
||||||
const slug = slugify(name);
|
const slug = slugify(name);
|
||||||
|
|
||||||
const article = await request(pool, tx)
|
const article = await runner.query(createArticleSql, {
|
||||||
.input('sku', sku)
|
sku,
|
||||||
.input('unitId', unitId)
|
unitId,
|
||||||
.input('unitamount', 1)
|
unitamount: 1,
|
||||||
.input('weight', 0)
|
weight: 0,
|
||||||
.query(CREATE);
|
});
|
||||||
|
|
||||||
const id = article.recordset[0].id;
|
const id = article.rows[0]?.id ?? article.insertId;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(addCategoryArticleSql, {
|
||||||
.input('categoryId', parentId)
|
categoryId: parentId,
|
||||||
.input('articleId', id)
|
articleId: id,
|
||||||
.query(ADD_CATEGORYARTICLE);
|
});
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(createArticleTextSql, {
|
||||||
.input('articleId', id)
|
articleId: id,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('name', name)
|
name,
|
||||||
.input('slug', slug)
|
slug,
|
||||||
.query(CREATE_ARTICLETEXT);
|
});
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(insertHistorySql, {
|
||||||
.input('articleId', id)
|
articleId: id,
|
||||||
.input('companyuserId', companyuserId)
|
companyuserId,
|
||||||
.input('text', `Artikel "${name}" angelegt`)
|
text: `Artikel "${name}" angelegt`,
|
||||||
.query(INSERT_HISTORY);
|
});
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -76,13 +83,15 @@ export default function createUnderParent(pool) {
|
|||||||
name,
|
name,
|
||||||
sku,
|
sku,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runCreate(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,83 +1,60 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
|
||||||
DELETE_ARTICLELIST,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
|
||||||
DELETE_BY_ID,
|
|
||||||
DELETE_OPTIONSLIST,
|
|
||||||
DELETE_PICS_BY_ARTICLE,
|
|
||||||
GET_CHILD_COUNT,
|
|
||||||
GET_LIST_IDS,
|
|
||||||
} from '../../providers/mssql/queries/articleDelete.js';
|
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import request from '../request.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an article by its SKU, refusing when it still has children.
|
* Deletes an article by its SKU, refusing when it still has children.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the article has children or is not found.
|
* @throws {Error} If the article has children or is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.article.delete(42, 'SKU-1');
|
* await store.article.delete(42, 'SKU-1');
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku) => {
|
return async (googleAuthId, sku, tx) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const runDelete = async (runner) => {
|
||||||
|
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const childCount = await pool
|
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
|
||||||
.request()
|
if (childCount.rows[0].count > 0) {
|
||||||
.input('articleId', articleId)
|
throw new Error('Article has children');
|
||||||
.query(GET_CHILD_COUNT);
|
}
|
||||||
|
|
||||||
if (childCount.recordset[0].count > 0) {
|
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId });
|
||||||
throw new Error('Article has children');
|
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
||||||
}
|
|
||||||
|
|
||||||
const listIds = await pool
|
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId });
|
||||||
.request()
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(GET_LIST_IDS);
|
|
||||||
|
|
||||||
const { articlelistId, optionslistId } = listIds.recordset[0] ?? {};
|
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId });
|
||||||
|
if (!deleted.rowsAffected) {
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(DELETE_PICS_BY_ARTICLE);
|
|
||||||
|
|
||||||
const deleted = await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(DELETE_BY_ID);
|
|
||||||
|
|
||||||
if (!deleted.rowsAffected[0]) {
|
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (articlelistId != null) {
|
if (articlelistId != null) {
|
||||||
await request(pool, tx)
|
await runner.query(queries.DELETE_ARTICLELIST, { id: articlelistId });
|
||||||
.input('id', articlelistId)
|
|
||||||
.query(DELETE_ARTICLELIST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionslistId != null) {
|
if (optionslistId != null) {
|
||||||
await request(pool, tx)
|
await runner.query(queries.DELETE_OPTIONSLIST, { id: optionslistId });
|
||||||
.input('id', optionslistId)
|
|
||||||
.query(DELETE_OPTIONSLIST);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await tx.commit();
|
if (tx) {
|
||||||
} catch (err) {
|
return runDelete(makeRunner(provider, tx));
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runDelete);
|
||||||
|
}
|
||||||
|
return runDelete(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { DELETE_BY_ARTICLE_AND_HASH } from '../../providers/mssql/queries/pic.js';
|
import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js';
|
||||||
|
import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function deleteImage(pool) {
|
export default function deleteImage(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an image from an article by its SKU and full image hash.
|
* Deletes an image from an article by its SKU and full image hash.
|
||||||
@@ -10,23 +13,29 @@ export default function deleteImage(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {string} fullHash - The full image hash to delete.
|
* @param {string} fullHash - The full image hash to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the image is not found.
|
* @throws {Error} If the image is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.article.deleteImage(42, 'SKU-1', 'abc123');
|
* await store.article.deleteImage(42, 'SKU-1', 'abc123');
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, fullHash) => {
|
return async (googleAuthId, sku, fullHash, tx) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const { articleId } = await resolve(googleAuthId, sku, tx);
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const result = await runner.query(sql, { articleId, fullHash });
|
||||||
|
|
||||||
const result = await pool
|
if (!result.rowsAffected) {
|
||||||
.request()
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('fullHash', fullHash)
|
|
||||||
.query(DELETE_BY_ARTICLE_AND_HASH);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
|
||||||
throw new Error('Image not found');
|
throw new Error('Image not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider?.dialect === 'sqlite3') {
|
||||||
|
if (runner.deletePic) {
|
||||||
|
await runner.deletePic(fullHash);
|
||||||
|
} else if (provider?.deletePic) {
|
||||||
|
await provider.deletePic(fullHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { GET_ARTICLE_BY_SKU_FULL } from '../../providers/mssql/queries/article.js';
|
import { GET_ARTICLE_BY_SKU_FULL as MSSQL_QUERY } from '../../providers/mssql/queries/article.js';
|
||||||
|
import { GET_ARTICLE_BY_SKU_FULL as SQLITE_QUERY } from '../../providers/sqlite3/queries/article.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function parseJsonArray(raw) {
|
function parseJsonArray(raw) {
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
@@ -6,13 +8,16 @@ function parseJsonArray(raw) {
|
|||||||
return Array.isArray(arr) ? arr : [];
|
return Array.isArray(arr) ? arr : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function getBySku(pool) {
|
export default function getBySku(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the full detail of an article by its SKU, including unit, brand,
|
* Returns the full detail of an article by its SKU, including unit, brand,
|
||||||
* categories, photos, EANs and prices.
|
* categories, photos, EANs and prices.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail.
|
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {name: string, thumbW: number, thumbH: number, thumbHash: string}|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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail.
|
||||||
* @throws {Error} If the article is not found or the user is not authorized.
|
* @throws {Error} If the article is not found or the user is not authorized.
|
||||||
*
|
*
|
||||||
@@ -20,16 +25,13 @@ export default function getBySku(pool) {
|
|||||||
* const article = await store.article.getBySku(42, 'SKU-1');
|
* const article = await store.article.getBySku(42, 'SKU-1');
|
||||||
* // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } }
|
* // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku) => {
|
return async (googleAuthId, sku, tx) => {
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
const result = await pool
|
const runner = makeRunner(provider, tx);
|
||||||
.request()
|
const result = await runner.query(sql, { sku, googleAuthId });
|
||||||
.input('sku', sku)
|
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(GET_ARTICLE_BY_SKU_FULL);
|
|
||||||
console.log(`[getBySku] single query ${(performance.now() - t0).toFixed(1)}ms`);
|
console.log(`[getBySku] single query ${(performance.now() - t0).toFixed(1)}ms`);
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,7 @@ export default function getBySku(pool) {
|
|||||||
throw new Error('Not authorized');
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson;
|
const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
import { LIST_BY_ARTICLE_ID } from '../../providers/mssql/queries/articlehistory.js';
|
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function getHistoryBySku(pool) {
|
export default function getHistoryBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the change history of an article by its SKU.
|
* Returns the change history of an article by its SKU.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||||
* // => [{ id, companyuserId, date, text }]
|
* // => [{ id, companyuserId, date, text }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku) => {
|
return async (googleAuthId, sku, tx) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const { articleId } = await resolve(googleAuthId, sku, tx);
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const result = await runner.query(sql, { articleId });
|
||||||
|
|
||||||
const result = await pool
|
return result.rows.map((row) => ({
|
||||||
.request()
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(LIST_BY_ARTICLE_ID);
|
|
||||||
|
|
||||||
return result.recordset.map((row) => ({
|
|
||||||
id: row.id,
|
id: row.id,
|
||||||
companyuserId: row.companyuserId ?? null,
|
companyuserId: row.companyuserId ?? null,
|
||||||
date: row.date,
|
date: row.date,
|
||||||
@@ -31,3 +32,4 @@ export default function getHistoryBySku(pool) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
|
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
||||||
|
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
|
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||||
|
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||||
import {
|
import {
|
||||||
ADD_EAN,
|
CREATE as MSSQL_CREATE_BRAND,
|
||||||
CREATE as CREATE_ARTICLE,
|
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME,
|
||||||
EAN_EXISTS,
|
} from '../../providers/mssql/queries/brand.js';
|
||||||
GET_ID_BY_SKU,
|
import {
|
||||||
SET_FATHER_SORT,
|
CREATE as SQLITE_CREATE_BRAND,
|
||||||
SET_IS_FATHER,
|
GET_BY_COMPANY_AND_NAME as SQLITE_GET_BRAND_BY_NAME,
|
||||||
UPDATE_BRAND,
|
} from '../../providers/sqlite3/queries/brand.js';
|
||||||
UPDATE_FATHER_BY_SKU,
|
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
||||||
} from '../../providers/mssql/queries/article.js';
|
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
|
||||||
import { ADD as ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
|
import {
|
||||||
import { CREATE as CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
CREATE as MSSQL_CREATE_TAX,
|
||||||
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
GET_BY_RATE as MSSQL_GET_TAX_BY_RATE,
|
||||||
import { GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
} from '../../providers/mssql/queries/tax.js';
|
||||||
import { CREATE as CREATE_BRAND, GET_BY_COMPANY_AND_NAME } from '../../providers/mssql/queries/brand.js';
|
import {
|
||||||
import { CREATE as CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
CREATE as SQLITE_CREATE_TAX,
|
||||||
import { CREATE as CREATE_TAX, GET_BY_RATE } from '../../providers/mssql/queries/tax.js';
|
GET_BY_RATE as SQLITE_GET_TAX_BY_RATE,
|
||||||
import request from '../request.js';
|
} from '../../providers/sqlite3/queries/tax.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import ensureByPath from '../category/ensureByPath.js';
|
import ensureByPath from '../category/ensureByPath.js';
|
||||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
|
||||||
// Normalizes a numeric value that may arrive as a string using German decimal
|
// Normalizes a numeric value that may arrive as a string using German decimal
|
||||||
@@ -34,29 +45,41 @@ function toNumber(value) {
|
|||||||
return Number.isFinite(n) ? n : null;
|
return Number.isFinite(n) ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function importRows(pool) {
|
export default function importRows(provider) {
|
||||||
const companyAccess = resolveSelectedCompanyAccess(pool);
|
const companyAccess = resolveSelectedCompanyAccess(provider);
|
||||||
const ensureUnit = ensureDefaultUnit(pool);
|
const ensureUnit = ensureDefaultUnit(provider);
|
||||||
const ensureCategoryByPath = ensureByPath(pool);
|
const ensureCategoryByPath = ensureByPath(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||||
|
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||||
|
const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME;
|
||||||
|
const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND;
|
||||||
|
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
|
||||||
|
const getTaxByRateSql = isSqlite ? SQLITE_GET_TAX_BY_RATE : MSSQL_GET_TAX_BY_RATE;
|
||||||
|
const createTaxSql = isSqlite ? SQLITE_CREATE_TAX : MSSQL_CREATE_TAX;
|
||||||
|
|
||||||
// Find or create a brand for a company by name (within the current tx).
|
// Find or create a brand for a company by name (within the current tx).
|
||||||
async function ensureBrand(tx, companyId, name) {
|
async function ensureBrand(runner, companyId, name) {
|
||||||
const existing = await request(pool, tx)
|
const existing = await runner.query(getBrandByNameSql, {
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.input('name', name)
|
name,
|
||||||
.query(GET_BY_COMPANY_AND_NAME);
|
});
|
||||||
|
|
||||||
if (existing.recordset[0]) {
|
if (existing.rows[0]) {
|
||||||
return existing.recordset[0].id;
|
return existing.rows[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await request(pool, tx)
|
const created = await runner.query(createBrandSql, {
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.input('name', name)
|
name,
|
||||||
.input('gsprpinfo', null)
|
gsprpinfo: null,
|
||||||
.query(CREATE_BRAND);
|
});
|
||||||
|
|
||||||
return created.recordset[0].id;
|
return created.rows[0]?.id ?? created.insertId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +105,7 @@ export default function importRows(pool) {
|
|||||||
* @param {Array<string>|string} [input.rows[].eans] - The EANs to set.
|
* @param {Array<string>|string} [input.rows[].eans] - The EANs to set.
|
||||||
* @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link.
|
* @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link.
|
||||||
* @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
|
* @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
|
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -94,14 +118,15 @@ export default function importRows(pool) {
|
|||||||
* });
|
* });
|
||||||
* // => { imported: [...], skipped: [...], errors: [...] }
|
* // => { imported: [...], skipped: [...], errors: [...] }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, rows }) => {
|
return async ({ googleAuthId, rows }, tx) => {
|
||||||
|
const baseRunner = makeRunner(provider, tx);
|
||||||
const {
|
const {
|
||||||
companyId,
|
companyId,
|
||||||
languageId,
|
languageId,
|
||||||
rootCategoryId,
|
rootCategoryId,
|
||||||
posCustomerGroupId,
|
posCustomerGroupId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
} = await companyAccess(googleAuthId);
|
} = await companyAccess(googleAuthId, baseRunner);
|
||||||
|
|
||||||
if (!Array.isArray(rows)) {
|
if (!Array.isArray(rows)) {
|
||||||
throw new Error('Invalid rows');
|
throw new Error('Invalid rows');
|
||||||
@@ -112,6 +137,16 @@ export default function importRows(pool) {
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
const usedEans = new Set();
|
const usedEans = new Set();
|
||||||
|
|
||||||
|
const runInTx = async (fn) => {
|
||||||
|
if (tx) {
|
||||||
|
return fn(baseRunner);
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(fn);
|
||||||
|
}
|
||||||
|
return fn(baseRunner);
|
||||||
|
};
|
||||||
|
|
||||||
// Pass 1: create articles (skip existing SKUs).
|
// Pass 1: create articles (skip existing SKUs).
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const sku = String(row?.sku ?? '').trim();
|
const sku = String(row?.sku ?? '').trim();
|
||||||
@@ -122,123 +157,113 @@ export default function importRows(pool) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await pool.request().input('sku', sku).query(GET_ID_BY_SKU);
|
const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku });
|
||||||
if (existing.recordset[0]) {
|
if (existing.rows[0]) {
|
||||||
skipped.push({ sku, name, reason: 'duplicate sku' });
|
skipped.push({ sku, name, reason: 'duplicate sku' });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tx;
|
|
||||||
try {
|
try {
|
||||||
tx = pool.transaction();
|
await runInTx(async (runner) => {
|
||||||
await tx.begin();
|
const unitId = await ensureUnit(runner, companyId, languageId);
|
||||||
|
const slug = slugify(name);
|
||||||
|
|
||||||
const unitId = await ensureUnit(tx, companyId, languageId);
|
const article = await runner.query(articleQueries.CREATE, {
|
||||||
const slug = slugify(name);
|
sku,
|
||||||
|
unitId,
|
||||||
|
unitamount: 1,
|
||||||
|
weight: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const article = await request(pool, tx)
|
const articleId = article.rows[0]?.id ?? article.insertId;
|
||||||
.input('sku', sku)
|
|
||||||
.input('unitId', unitId)
|
|
||||||
.input('unitamount', 1)
|
|
||||||
.input('weight', 0)
|
|
||||||
.query(CREATE_ARTICLE);
|
|
||||||
|
|
||||||
const articleId = article.recordset[0].id;
|
// Resolve the parent category id (explicit id or breadcrumb path).
|
||||||
|
let parentId = row.parentId;
|
||||||
// Resolve the parent category id (explicit id or breadcrumb path).
|
if (parentId == null && row.parentCategory) {
|
||||||
let parentId = row.parentId;
|
parentId = await ensureCategoryByPath(
|
||||||
if (parentId == null && row.parentCategory) {
|
{ googleAuthId, parentId: rootCategoryId, path: row.parentCategory },
|
||||||
parentId = await ensureCategoryByPath(
|
runner,
|
||||||
{ googleAuthId, parentId: rootCategoryId, path: row.parentCategory },
|
);
|
||||||
tx,
|
}
|
||||||
);
|
if (parentId == null) {
|
||||||
}
|
throw new Error('Parent category required');
|
||||||
if (parentId == null) {
|
|
||||||
throw new Error('Parent category required');
|
|
||||||
}
|
|
||||||
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('categoryId', parentId)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(ADD_CATEGORYARTICLE);
|
|
||||||
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('languageId', languageId)
|
|
||||||
.input('name', name)
|
|
||||||
.input('slug', slug)
|
|
||||||
.query(CREATE_ARTICLETEXT);
|
|
||||||
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('companyuserId', companyuserId)
|
|
||||||
.input('text', `Artikel "${name}" importiert`)
|
|
||||||
.query(INSERT_HISTORY);
|
|
||||||
|
|
||||||
// Brand (upsert by name).
|
|
||||||
const brandName = row.brand != null ? String(row.brand).trim() : '';
|
|
||||||
if (brandName) {
|
|
||||||
const brandId = await ensureBrand(tx, companyId, brandName);
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('brandId', brandId)
|
|
||||||
.query(UPDATE_BRAND);
|
|
||||||
}
|
|
||||||
|
|
||||||
// POS price (net/tax).
|
|
||||||
const net = toNumber(row.net);
|
|
||||||
const taxRate = toNumber(row.tax) ?? 0;
|
|
||||||
if (net != null) {
|
|
||||||
const taxResult = await request(pool, tx)
|
|
||||||
.input('rate', taxRate)
|
|
||||||
.query(GET_BY_RATE);
|
|
||||||
let taxId = taxResult.recordset[0]?.id;
|
|
||||||
if (!taxId) {
|
|
||||||
const created = await request(pool, tx)
|
|
||||||
.input('rate', taxRate)
|
|
||||||
.query(CREATE_TAX);
|
|
||||||
taxId = created.recordset[0].id;
|
|
||||||
}
|
}
|
||||||
await request(pool, tx)
|
|
||||||
.input('taxId', taxId)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('minmultiplier', 1)
|
|
||||||
.input('customerGroupId', posCustomerGroupId)
|
|
||||||
.input('net', net)
|
|
||||||
.query(CREATE_PRICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EANs (array, or a single value; skip duplicates within the import).
|
await runner.query(addCategoryArticleSql, {
|
||||||
const rawEans = row.eans;
|
categoryId: parentId,
|
||||||
const eans = Array.isArray(rawEans)
|
articleId,
|
||||||
? rawEans
|
});
|
||||||
: rawEans == null || rawEans === ''
|
|
||||||
? []
|
|
||||||
: [rawEans];
|
|
||||||
for (const ean of eans) {
|
|
||||||
const clean = String(ean ?? '').trim();
|
|
||||||
if (!clean || usedEans.has(clean)) continue;
|
|
||||||
const used = await request(pool, tx)
|
|
||||||
.input('ean', clean)
|
|
||||||
.query(EAN_EXISTS);
|
|
||||||
if (used.recordset[0]) continue;
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('ean', clean)
|
|
||||||
.query(ADD_EAN);
|
|
||||||
usedEans.add(clean);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.commit();
|
await runner.query(createArticleTextSql, {
|
||||||
imported.push({ id: articleId, sku, name });
|
articleId,
|
||||||
|
languageId,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Artikel "${name}" importiert`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Brand (upsert by name).
|
||||||
|
const brandName = row.brand != null ? String(row.brand).trim() : '';
|
||||||
|
if (brandName) {
|
||||||
|
const brandId = await ensureBrand(runner, companyId, brandName);
|
||||||
|
await runner.query(articleQueries.UPDATE_BRAND, {
|
||||||
|
articleId,
|
||||||
|
brandId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POS price (net/tax).
|
||||||
|
const net = toNumber(row.net);
|
||||||
|
const taxRate = toNumber(row.tax) ?? 0;
|
||||||
|
if (net != null) {
|
||||||
|
const taxResult = await runner.query(getTaxByRateSql, {
|
||||||
|
rate: taxRate,
|
||||||
|
});
|
||||||
|
let taxId = taxResult.rows[0]?.id;
|
||||||
|
if (!taxId) {
|
||||||
|
const created = await runner.query(createTaxSql, {
|
||||||
|
rate: taxRate,
|
||||||
|
});
|
||||||
|
taxId = created.rows[0]?.id ?? created.insertId;
|
||||||
|
}
|
||||||
|
await runner.query(createPriceSql, {
|
||||||
|
taxId,
|
||||||
|
articleId,
|
||||||
|
minmultiplier: 1,
|
||||||
|
customerGroupId: posCustomerGroupId,
|
||||||
|
net,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// EANs (array, or a single value; skip duplicates within the import).
|
||||||
|
const rawEans = row.eans;
|
||||||
|
const eans = Array.isArray(rawEans)
|
||||||
|
? rawEans
|
||||||
|
: rawEans == null || rawEans === ''
|
||||||
|
? []
|
||||||
|
: [rawEans];
|
||||||
|
for (const ean of eans) {
|
||||||
|
const clean = String(ean ?? '').trim();
|
||||||
|
if (!clean || usedEans.has(clean)) continue;
|
||||||
|
const used = await runner.query(articleQueries.EAN_EXISTS, {
|
||||||
|
ean: clean,
|
||||||
|
});
|
||||||
|
if (used.rows[0]) continue;
|
||||||
|
await runner.query(articleQueries.ADD_EAN, {
|
||||||
|
articleId,
|
||||||
|
ean: clean,
|
||||||
|
});
|
||||||
|
usedEans.add(clean);
|
||||||
|
}
|
||||||
|
|
||||||
|
imported.push({ id: articleId, sku, name });
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (tx) {
|
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errors.push({ sku, name, error: err.message ?? String(err) });
|
errors.push({ sku, name, error: err.message ?? String(err) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,50 +275,35 @@ export default function importRows(pool) {
|
|||||||
row?.fatherSku != null ? String(row.fatherSku).trim() : '';
|
row?.fatherSku != null ? String(row.fatherSku).trim() : '';
|
||||||
if (!sku || !fatherSku) continue;
|
if (!sku || !fatherSku) continue;
|
||||||
|
|
||||||
let tx;
|
|
||||||
try {
|
try {
|
||||||
tx = pool.transaction();
|
await runInTx(async (runner) => {
|
||||||
await tx.begin();
|
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku });
|
||||||
|
const childId = child.rows[0]?.articleId;
|
||||||
const child = await request(pool, tx)
|
if (childId) {
|
||||||
.input('sku', sku)
|
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku });
|
||||||
.query(GET_ID_BY_SKU);
|
const fatherId = father.rows[0]?.articleId;
|
||||||
const childId = child.recordset[0]?.articleId;
|
if (fatherId && fatherId !== childId) {
|
||||||
if (childId) {
|
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
||||||
const father = await request(pool, tx)
|
sku,
|
||||||
.input('sku', fatherSku)
|
fatherArticleId: fatherId,
|
||||||
.query(GET_ID_BY_SKU);
|
});
|
||||||
const fatherId = father.recordset[0]?.articleId;
|
await runner.query(articleQueries.SET_IS_FATHER, {
|
||||||
if (fatherId && fatherId !== childId) {
|
articleId: fatherId,
|
||||||
await request(pool, tx)
|
});
|
||||||
.input('sku', sku)
|
const childCount = await runner.query(getChildCountSql, {
|
||||||
.input('fatherArticleId', fatherId)
|
articleId: fatherId,
|
||||||
.query(UPDATE_FATHER_BY_SKU);
|
});
|
||||||
await request(pool, tx)
|
const count = childCount.rows[0].count;
|
||||||
.input('articleId', fatherId)
|
// The child's sort is its 0-based position among the father's children.
|
||||||
.query(SET_IS_FATHER);
|
await runner.query(articleQueries.SET_FATHER_SORT, {
|
||||||
const childCount = await request(pool, tx)
|
articleId: childId,
|
||||||
.input('articleId', fatherId)
|
fatherArticleId: fatherId,
|
||||||
.query(GET_CHILD_COUNT);
|
fathersort: count - 1,
|
||||||
const count = childCount.recordset[0].count;
|
});
|
||||||
// The child's sort is its 0-based position among the father's children.
|
}
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', childId)
|
|
||||||
.input('fatherArticleId', fatherId)
|
|
||||||
.input('fathersort', count - 1)
|
|
||||||
.query(SET_FATHER_SORT);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
} catch {
|
} catch {
|
||||||
if (tx) {
|
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Non-fatal: article was already imported in pass 1.
|
// Non-fatal: article was already imported in pass 1.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { GET_ARTICLE_EXPORT, GET_CATEGORY_TREE_FLAT } from '../../providers/mssql/queries/article.js';
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
const ROOT_CATEGORY_NAME = 'Home';
|
const ROOT_CATEGORY_NAME = 'Home';
|
||||||
|
|
||||||
@@ -10,9 +12,11 @@ function parseJsonArray(raw) {
|
|||||||
return Array.isArray(arr) ? arr : [];
|
return Array.isArray(arr) ? arr : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function listExport(pool) {
|
export default function listExport(provider) {
|
||||||
const companyAccess = resolveSelectedCompanyAccess(pool);
|
const companyAccess = resolveSelectedCompanyAccess(provider);
|
||||||
const categoryAccess = resolveCategoryAccess(pool);
|
const categoryAccess = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns articles for export, optionally scoped to a category, including
|
* Returns articles for export, optionally scoped to a category, including
|
||||||
@@ -20,6 +24,7 @@ export default function listExport(pool) {
|
|||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number|null} [categoryId] - Optional category id to scope the export to; defaults to the company root.
|
* @param {number|null} [categoryId] - Optional category id to scope the export to; defaults to the company root.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<object>>} The articles for export.
|
* @returns {Promise<Array<object>>} The articles for export.
|
||||||
* @throws {Error} If the category is not reachable from the selected company.
|
* @throws {Error} If the category is not reachable from the selected company.
|
||||||
*
|
*
|
||||||
@@ -27,16 +32,17 @@ export default function listExport(pool) {
|
|||||||
* const articles = await store.article.listExport(42, null);
|
* const articles = await store.article.listExport(42, null);
|
||||||
* // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
* // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, categoryId) => {
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const { languageId, posCustomerGroupId, rootCategoryId } =
|
const { languageId, posCustomerGroupId, rootCategoryId } =
|
||||||
await companyAccess(googleAuthId);
|
await companyAccess(googleAuthId, runner);
|
||||||
|
|
||||||
// Default to the company's root category when no specific category is given.
|
// Default to the company's root category when no specific category is given.
|
||||||
let exportCategoryId = rootCategoryId;
|
let exportCategoryId = rootCategoryId;
|
||||||
|
|
||||||
if (categoryId != null) {
|
if (categoryId != null) {
|
||||||
// The requested category must be reachable via the selected company's root.
|
// The requested category must be reachable via the selected company's root.
|
||||||
const access = await categoryAccess(googleAuthId, categoryId);
|
const access = await categoryAccess(googleAuthId, categoryId, runner);
|
||||||
if (access.rootCategoryId !== rootCategoryId) {
|
if (access.rootCategoryId !== rootCategoryId) {
|
||||||
throw new Error('Not authorized');
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
@@ -44,22 +50,20 @@ export default function listExport(pool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [result, treeResult] = await Promise.all([
|
const [result, treeResult] = await Promise.all([
|
||||||
pool
|
runner.query(queries.GET_ARTICLE_EXPORT, {
|
||||||
.request()
|
categoryId: exportCategoryId,
|
||||||
.input('categoryId', exportCategoryId)
|
languageId,
|
||||||
.input('languageId', languageId)
|
posCustomerGroupId,
|
||||||
.input('posCustomerGroupId', posCustomerGroupId)
|
}),
|
||||||
.query(GET_ARTICLE_EXPORT),
|
runner.query(queries.GET_CATEGORY_TREE_FLAT, {
|
||||||
pool
|
rootId: rootCategoryId,
|
||||||
.request()
|
languageId,
|
||||||
.input('rootId', rootCategoryId)
|
}),
|
||||||
.input('languageId', languageId)
|
|
||||||
.query(GET_CATEGORY_TREE_FLAT),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// id -> { id, parentId, name } for breadcrumb resolution.
|
// id -> { id, parentId, name } for breadcrumb resolution.
|
||||||
const catById = new Map();
|
const catById = new Map();
|
||||||
for (const node of treeResult.recordset) {
|
for (const node of treeResult.rows) {
|
||||||
catById.set(String(node.id), node);
|
catById.set(String(node.id), node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +71,10 @@ export default function listExport(pool) {
|
|||||||
const displayName = (node) =>
|
const displayName = (node) =>
|
||||||
String(node.id) === String(rootCategoryId) ? ROOT_CATEGORY_NAME : (node.name ?? '').trim();
|
String(node.id) === String(rootCategoryId) ? ROOT_CATEGORY_NAME : (node.name ?? '').trim();
|
||||||
|
|
||||||
const breadcrumb = (categoryId) => {
|
const breadcrumb = (cId) => {
|
||||||
const crumbs = [];
|
const crumbs = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
let id = categoryId;
|
let id = cId;
|
||||||
while (id != null && !seen.has(String(id))) {
|
while (id != null && !seen.has(String(id))) {
|
||||||
seen.add(String(id));
|
seen.add(String(id));
|
||||||
const node = catById.get(String(id));
|
const node = catById.get(String(id));
|
||||||
@@ -82,7 +86,7 @@ export default function listExport(pool) {
|
|||||||
return crumbs.join('·');
|
return crumbs.join('·');
|
||||||
};
|
};
|
||||||
|
|
||||||
return result.recordset.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sku: row.sku,
|
sku: row.sku,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -102,3 +106,4 @@ export default function listExport(pool) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
import { SEARCH_ARTICLES } from '../../providers/mssql/queries/article.js';
|
import { SEARCH_ARTICLES as MSSQL_SEARCH_ARTICLES } from '../../providers/mssql/queries/article.js';
|
||||||
|
import { SEARCH_ARTICLES as SQLITE_SEARCH_ARTICLES } from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function listSearchIndex(pool) {
|
export default function listSearchIndex(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SEARCH_ARTICLES : MSSQL_SEARCH_ARTICLES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a lightweight search index of all articles for the user's selected company.
|
* Returns a lightweight search index of all articles for the user's selected company.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>} The article search index.
|
* @returns {Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>} The article search index.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const articles = await store.article.listSearchIndex(42);
|
* const articles = await store.article.listSearchIndex(42);
|
||||||
* // => [{ id, sku, name, eans, categoryIds }]
|
* // => [{ id, sku, name, eans, categoryIds }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const { companyId, rootCategoryId, languageId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { rootCategoryId, languageId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
rootCategoryId,
|
||||||
.input('rootCategoryId', rootCategoryId)
|
languageId,
|
||||||
.input('languageId', languageId)
|
});
|
||||||
.query(SEARCH_ARTICLES);
|
|
||||||
|
|
||||||
return result.recordset.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sku: row.sku,
|
sku: row.sku,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
@@ -34,3 +38,4 @@ export default function listSearchIndex(pool) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import * as mssqlPic from '../../providers/mssql/queries/articlepic.js';
|
||||||
LIST_HASHES_BY_ARTICLE,
|
import * as sqlitePic from '../../providers/sqlite3/queries/articlepic.js';
|
||||||
SET_TEMP_SORTS,
|
|
||||||
UPDATE_SORT_BY_HASH,
|
|
||||||
} from '../../providers/mssql/queries/articlepic.js';
|
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import request from '../request.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function orderImages(pool) {
|
export default function orderImages(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqlitePic : mssqlPic;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the images of an article by its SKU.
|
* Reorders the images of an article by its SKU.
|
||||||
@@ -16,6 +15,7 @@ export default function orderImages(pool) {
|
|||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {object} input - The reorder input.
|
* @param {object} input - The reorder input.
|
||||||
* @param {Array<string>} input.fullHashes - The full image hashes in their new order.
|
* @param {Array<string>} input.fullHashes - The full image hashes in their new order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{fullHashes: Array<string>}>} The applied order.
|
* @returns {Promise<{fullHashes: Array<string>}>} The applied order.
|
||||||
* @throws {Error} If the order is invalid.
|
* @throws {Error} If the order is invalid.
|
||||||
*
|
*
|
||||||
@@ -23,62 +23,53 @@ export default function orderImages(pool) {
|
|||||||
* const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] });
|
* const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] });
|
||||||
* // => { fullHashes: ['a', 'b'] }
|
* // => { fullHashes: ['a', 'b'] }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, { fullHashes }) => {
|
return async (googleAuthId, sku, { fullHashes }, tx) => {
|
||||||
if (!Array.isArray(fullHashes)) {
|
if (!Array.isArray(fullHashes)) {
|
||||||
throw new Error('Invalid image order');
|
throw new Error('Invalid image order');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const runOrder = async (runner) => {
|
||||||
|
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const current = await pool
|
const current = await runner.query(queries.LIST_HASHES_BY_ARTICLE, { articleId });
|
||||||
.request()
|
const existing = new Set(current.rows.map((r) => r.fullHash));
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(LIST_HASHES_BY_ARTICLE);
|
|
||||||
|
|
||||||
const existing = new Set(current.recordset.map((r) => r.fullHash));
|
if (fullHashes.length !== existing.size) {
|
||||||
|
|
||||||
if (fullHashes.length !== existing.size) {
|
|
||||||
throw new Error('Invalid image order');
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
for (const fullHash of fullHashes) {
|
|
||||||
if (!existing.has(fullHash) || seen.has(fullHash)) {
|
|
||||||
throw new Error('Invalid image order');
|
throw new Error('Invalid image order');
|
||||||
}
|
}
|
||||||
seen.add(fullHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const seen = new Set();
|
||||||
await tx.begin();
|
for (const fullHash of fullHashes) {
|
||||||
|
if (!existing.has(fullHash) || seen.has(fullHash)) {
|
||||||
|
throw new Error('Invalid image order');
|
||||||
|
}
|
||||||
|
seen.add(fullHash);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
await runner.query(queries.SET_TEMP_SORTS, { articleId });
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(SET_TEMP_SORTS);
|
|
||||||
|
|
||||||
for (let sort = 0; sort < fullHashes.length; sort++) {
|
for (let sort = 0; sort < fullHashes.length; sort++) {
|
||||||
const update = await request(pool, tx)
|
const update = await runner.query(queries.UPDATE_SORT_BY_HASH, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('fullHash', fullHashes[sort])
|
fullHash: fullHashes[sort],
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(UPDATE_SORT_BY_HASH);
|
});
|
||||||
|
|
||||||
if (!update.rowsAffected[0]) {
|
if (!update.rowsAffected) {
|
||||||
throw new Error('Invalid image order');
|
throw new Error('Invalid image order');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return { fullHashes };
|
return { fullHashes };
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runOrder(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runOrder);
|
||||||
|
}
|
||||||
|
return runOrder(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import {
|
import * as mssqlDetail from '../../providers/mssql/queries/articleDetail.js';
|
||||||
GET_CATEGORY_IDS_BY_ARTICLE_ID,
|
import * as sqliteDetail from '../../providers/sqlite3/queries/articleDetail.js';
|
||||||
GET_DETAIL_BY_ARTICLE_ID,
|
import makeRunner from '../runner.js';
|
||||||
GET_EANS_BY_ARTICLE_ID,
|
|
||||||
GET_PHOTOS_BY_ARTICLE_ID,
|
|
||||||
GET_PRICES_BY_ARTICLE_ID,
|
|
||||||
} from '../../providers/mssql/queries/articleDetail.js';
|
|
||||||
|
|
||||||
function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
|
function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
|
||||||
return {
|
return {
|
||||||
@@ -41,31 +37,32 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function queryBySku(pool) {
|
export default function queryBySku(provider) {
|
||||||
return async (articleId, languageId) => {
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteDetail : mssqlDetail;
|
||||||
|
|
||||||
|
return async (articleId, languageId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const [detail, categories, photos, eans, prices] = await Promise.all([
|
const [detail, categories, photos, eans, prices] = await Promise.all([
|
||||||
pool
|
runner.query(queries.GET_DETAIL_BY_ARTICLE_ID, { articleId, languageId }),
|
||||||
.request()
|
runner.query(queries.GET_CATEGORY_IDS_BY_ARTICLE_ID, { articleId }),
|
||||||
.input('articleId', articleId)
|
runner.query(queries.GET_PHOTOS_BY_ARTICLE_ID, { articleId }),
|
||||||
.input('languageId', languageId)
|
runner.query(queries.GET_EANS_BY_ARTICLE_ID, { articleId }),
|
||||||
.query(GET_DETAIL_BY_ARTICLE_ID),
|
runner.query(queries.GET_PRICES_BY_ARTICLE_ID, { articleId }),
|
||||||
pool.request().input('articleId', articleId).query(GET_CATEGORY_IDS_BY_ARTICLE_ID),
|
|
||||||
pool.request().input('articleId', articleId).query(GET_PHOTOS_BY_ARTICLE_ID),
|
|
||||||
pool.request().input('articleId', articleId).query(GET_EANS_BY_ARTICLE_ID),
|
|
||||||
pool.request().input('articleId', articleId).query(GET_PRICES_BY_ARTICLE_ID),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const row = detail.recordset[0];
|
const row = detail.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shapeArticleDetail(
|
return shapeArticleDetail(
|
||||||
row,
|
row,
|
||||||
categories.recordset,
|
categories.rows,
|
||||||
photos.recordset,
|
photos.rows,
|
||||||
eans.recordset,
|
eans.rows,
|
||||||
prices.recordset,
|
prices.rows,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { RESOLVE_ARTICLE_ACCESS_BY_SKU } from '../../providers/mssql/queries/article.js';
|
import { RESOLVE_ARTICLE_ACCESS_BY_SKU as MSSQL_RESOLVE } from '../../providers/mssql/queries/article.js';
|
||||||
|
import { RESOLVE_ARTICLE_ACCESS_BY_SKU as SQLITE_RESOLVE } from '../../providers/sqlite3/queries/article.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function resolveArticleBySku(pool) {
|
export default function resolveArticleBySku(provider) {
|
||||||
return async (googleAuthId, sku) => {
|
const sql =
|
||||||
const result = await pool
|
provider?.dialect === 'sqlite3' ? SQLITE_RESOLVE : MSSQL_RESOLVE;
|
||||||
.request()
|
|
||||||
.input('sku', sku)
|
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(RESOLVE_ARTICLE_ACCESS_BY_SKU);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
return async (googleAuthId, sku, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
const result = await runner.query(sql, { sku, googleAuthId });
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
if (!row?.articleId || !row?.categoryId) {
|
if (!row?.articleId || !row?.categoryId) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
@@ -30,3 +32,4 @@ export default function resolveArticleBySku(pool) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
GET_BRAND_BY_COMPANY_AND_ID,
|
GET_BRAND_BY_COMPANY_AND_ID as MSSQL_GET_BRAND,
|
||||||
UPDATE_BRAND,
|
UPDATE_BRAND as MSSQL_UPDATE_BRAND,
|
||||||
} from '../../providers/mssql/queries/article.js';
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
GET_BRAND_BY_COMPANY_AND_ID as SQLITE_GET_BRAND,
|
||||||
|
UPDATE_BRAND as SQLITE_UPDATE_BRAND,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setBrandIdBySku(pool) {
|
export default function setBrandIdBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const getBrandSql = isSqlite ? SQLITE_GET_BRAND : MSSQL_GET_BRAND;
|
||||||
|
const updateBrandSql = isSqlite ? SQLITE_UPDATE_BRAND : MSSQL_UPDATE_BRAND;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets (or clears) the brand of an article by its SKU.
|
* Sets (or clears) the brand of an article by its SKU.
|
||||||
@@ -15,6 +24,7 @@ export default function setBrandIdBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {number|null} brandId - The brand id, or `null` to clear the brand.
|
* @param {number|null} brandId - The brand id, or `null` to clear the brand.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If the brand or article is not found.
|
* @throws {Error} If the brand or article is not found.
|
||||||
*
|
*
|
||||||
@@ -22,10 +32,12 @@ export default function setBrandIdBySku(pool) {
|
|||||||
* const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3);
|
* const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3);
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, brandId) => {
|
return async (googleAuthId, sku, brandId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const { articleId, languageId, companyId } = await resolve(
|
const { articleId, languageId, companyId } = await resolve(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
sku,
|
sku,
|
||||||
|
runner,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (brandId !== null && !Number.isFinite(brandId)) {
|
if (brandId !== null && !Number.isFinite(brandId)) {
|
||||||
@@ -34,28 +46,26 @@ export default function setBrandIdBySku(pool) {
|
|||||||
|
|
||||||
let resolvedBrandId = null;
|
let resolvedBrandId = null;
|
||||||
if (brandId !== null) {
|
if (brandId !== null) {
|
||||||
const brand = await pool
|
const brand = await runner.query(getBrandSql, {
|
||||||
.request()
|
brandId,
|
||||||
.input('brandId', brandId)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(GET_BRAND_BY_COMPANY_AND_ID);
|
if (!brand.rows[0]) {
|
||||||
if (!brand.recordset[0]) {
|
|
||||||
throw new Error('Brand not found');
|
throw new Error('Brand not found');
|
||||||
}
|
}
|
||||||
resolvedBrandId = brandId;
|
resolvedBrandId = brandId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(updateBrandSql, {
|
||||||
.request()
|
articleId,
|
||||||
.input('articleId', articleId)
|
brandId: resolvedBrandId,
|
||||||
.input('brandId', resolvedBrandId)
|
});
|
||||||
.query(UPDATE_BRAND);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
const article = await load(articleId, languageId, runner);
|
||||||
if (!article) {
|
if (!article) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
@@ -63,3 +73,4 @@ export default function setBrandIdBySku(pool) {
|
|||||||
return article;
|
return article;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
ADD_CATEGORY,
|
ADD_CATEGORY as MSSQL_ADD_CATEGORY,
|
||||||
DELETE_CATEGORIES_BY_ARTICLE,
|
DELETE_CATEGORIES_BY_ARTICLE as MSSQL_DELETE_CATEGORIES,
|
||||||
} from '../../providers/mssql/queries/article.js';
|
} from '../../providers/mssql/queries/article.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
ADD_CATEGORY as SQLITE_ADD_CATEGORY,
|
||||||
|
DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setCategoryIdsBySku(pool) {
|
export default function setCategoryIdsBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const categoryAccess = resolveCategoryAccess(pool);
|
const categoryAccess = resolveCategoryAccess(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY;
|
||||||
|
const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the category membership of an article by its SKU.
|
* Replaces the category membership of an article by its SKU.
|
||||||
@@ -18,6 +26,7 @@ export default function setCategoryIdsBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {Array<number>} categoryIds - The category ids the article should belong to (at least one).
|
* @param {Array<number>} categoryIds - The category ids the article should belong to (at least one).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If the category ids are invalid or the article is not found.
|
* @throws {Error} If the category ids are invalid or the article is not found.
|
||||||
*
|
*
|
||||||
@@ -25,7 +34,7 @@ export default function setCategoryIdsBySku(pool) {
|
|||||||
* const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]);
|
* const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]);
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, categoryIds) => {
|
return async (googleAuthId, sku, categoryIds, tx) => {
|
||||||
if (!Array.isArray(categoryIds)) {
|
if (!Array.isArray(categoryIds)) {
|
||||||
throw new Error('Invalid categoryIds');
|
throw new Error('Invalid categoryIds');
|
||||||
}
|
}
|
||||||
@@ -42,49 +51,45 @@ export default function setCategoryIdsBySku(pool) {
|
|||||||
throw new Error('Article must have at least one category');
|
throw new Error('Article must have at least one category');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, languageId, companyId } = await resolve(
|
const runSet = async (runner) => {
|
||||||
googleAuthId,
|
const { articleId, languageId, companyId } = await resolve(
|
||||||
sku,
|
googleAuthId,
|
||||||
);
|
sku,
|
||||||
|
runner,
|
||||||
|
);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const categoryId of normalized) {
|
for (const categoryId of normalized) {
|
||||||
// Ensures the category belongs to the same company as the article.
|
// Ensures the category belongs to the same company as the article.
|
||||||
const access = await categoryAccess(googleAuthId, categoryId);
|
const access = await categoryAccess(googleAuthId, categoryId, runner);
|
||||||
if (access.companyId !== companyId) {
|
if (access.companyId !== companyId) {
|
||||||
throw new Error('Invalid categoryIds');
|
throw new Error('Invalid categoryIds');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(deleteCategoriesSql, { articleId });
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(DELETE_CATEGORIES_BY_ARTICLE);
|
|
||||||
|
|
||||||
for (const categoryId of normalized) {
|
for (const categoryId of normalized) {
|
||||||
await request(pool, tx)
|
await runner.query(addCategorySql, {
|
||||||
.input('categoryId', categoryId)
|
categoryId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.query(ADD_CATEGORY);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
const article = await load(articleId, languageId, runner);
|
||||||
} catch (err) {
|
if (!article) {
|
||||||
try {
|
throw new Error('Article not found');
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
return article;
|
||||||
if (!article) {
|
};
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return article;
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
ADD_EAN,
|
ADD_EAN as MSSQL_ADD_EAN,
|
||||||
DELETE_EANS_BY_ARTICLE,
|
DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS,
|
||||||
} from '../../providers/mssql/queries/article.js';
|
} from '../../providers/mssql/queries/article.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
ADD_EAN as SQLITE_ADD_EAN,
|
||||||
|
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setEansBySku(pool) {
|
export default function setEansBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN;
|
||||||
|
const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the EAN list of an article by its SKU.
|
* Replaces the EAN list of an article by its SKU.
|
||||||
@@ -16,6 +24,7 @@ export default function setEansBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {Array<string>} eans - The EANs to set (must be unique, non-empty strings).
|
* @param {Array<string>} eans - The EANs to set (must be unique, non-empty strings).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If the EANs are invalid or the article is not found.
|
* @throws {Error} If the EANs are invalid or the article is not found.
|
||||||
*
|
*
|
||||||
@@ -23,7 +32,7 @@ export default function setEansBySku(pool) {
|
|||||||
* const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']);
|
* const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']);
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, eans) => {
|
return async (googleAuthId, sku, eans, tx) => {
|
||||||
if (!Array.isArray(eans)) {
|
if (!Array.isArray(eans)) {
|
||||||
throw new Error('Invalid eans');
|
throw new Error('Invalid eans');
|
||||||
}
|
}
|
||||||
@@ -41,38 +50,30 @@ export default function setEansBySku(pool) {
|
|||||||
normalized.push(clean);
|
normalized.push(clean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, languageId } = await resolve(googleAuthId, sku);
|
const runSet = async (runner) => {
|
||||||
|
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
await runner.query(deleteEansSql, { articleId });
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(DELETE_EANS_BY_ARTICLE);
|
|
||||||
|
|
||||||
for (const ean of normalized) {
|
for (const ean of normalized) {
|
||||||
await request(pool, tx)
|
await runner.query(addEanSql, { articleId, ean });
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('ean', ean)
|
|
||||||
.query(ADD_EAN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
const article = await load(articleId, languageId, runner);
|
||||||
} catch (err) {
|
if (!article) {
|
||||||
try {
|
throw new Error('Article not found');
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
return article;
|
||||||
if (!article) {
|
};
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return article;
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import {
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
GET_ID_BY_SKU,
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
SET_FATHER_SORT,
|
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||||
SET_IS_FATHER,
|
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||||
UPDATE_FATHER_BY_SKU,
|
|
||||||
} from '../../providers/mssql/queries/article.js';
|
|
||||||
import { GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setFatherBySku(pool) {
|
export default function setFatherBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links an article to a father article (V/K compound) by their SKUs.
|
* Links an article to a father article (V/K compound) by their SKUs.
|
||||||
@@ -19,6 +20,7 @@ export default function setFatherBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The child article SKU.
|
* @param {string} sku - The child article SKU.
|
||||||
* @param {string|null} fatherSku - The father article SKU, or `null` to unlink.
|
* @param {string|null} fatherSku - The father article SKU, or `null` to unlink.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated child article.
|
* @returns {Promise<object>} The updated child article.
|
||||||
* @throws {Error} If the article or father is not found, or the link is invalid.
|
* @throws {Error} If the article or father is not found, or the link is invalid.
|
||||||
*
|
*
|
||||||
@@ -26,73 +28,67 @@ export default function setFatherBySku(pool) {
|
|||||||
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
|
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
|
||||||
* // => { id, sku, name, fatherSku: '42001190', ... }
|
* // => { id, sku, name, fatherSku: '42001190', ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, fatherSku) => {
|
return async (googleAuthId, sku, fatherSku, tx) => {
|
||||||
const { articleId, languageId } = await resolve(googleAuthId, sku);
|
const runSet = async (runner) => {
|
||||||
|
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
if (fatherSku == null || fatherSku === '') {
|
if (fatherSku == null || fatherSku === '') {
|
||||||
await request(pool)
|
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||||
.input('sku', sku)
|
sku,
|
||||||
.input('fatherArticleId', null)
|
fatherArticleId: null,
|
||||||
.query(UPDATE_FATHER_BY_SKU);
|
});
|
||||||
const unlinked = await load(articleId, languageId);
|
const unlinked = await load(articleId, languageId, runner);
|
||||||
if (!unlinked) {
|
if (!unlinked) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
|
}
|
||||||
|
return unlinked;
|
||||||
}
|
}
|
||||||
return unlinked;
|
|
||||||
}
|
|
||||||
|
|
||||||
const father = await pool
|
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku });
|
||||||
.request()
|
const fatherId = father.rows[0]?.articleId;
|
||||||
.input('sku', fatherSku)
|
if (!fatherId) {
|
||||||
.query(GET_ID_BY_SKU);
|
throw new Error('Father not found');
|
||||||
const fatherId = father.recordset[0]?.articleId;
|
}
|
||||||
if (!fatherId) {
|
if (fatherId === articleId) {
|
||||||
throw new Error('Father not found');
|
throw new Error('Invalid father');
|
||||||
}
|
}
|
||||||
if (fatherId === articleId) {
|
|
||||||
throw new Error('Invalid father');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||||
await tx.begin();
|
sku,
|
||||||
|
fatherArticleId: fatherId,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
await runner.query(queries.SET_IS_FATHER, {
|
||||||
await request(pool, tx)
|
articleId: fatherId,
|
||||||
.input('sku', sku)
|
});
|
||||||
.input('fatherArticleId', fatherId)
|
|
||||||
.query(UPDATE_FATHER_BY_SKU);
|
|
||||||
|
|
||||||
await request(pool, tx)
|
const childCount = await runner.query(getChildCountSql, {
|
||||||
.input('articleId', fatherId)
|
articleId: fatherId,
|
||||||
.query(SET_IS_FATHER);
|
});
|
||||||
|
const count = childCount.rows[0].count;
|
||||||
const childCount = await request(pool, tx)
|
|
||||||
.input('articleId', fatherId)
|
|
||||||
.query(GET_CHILD_COUNT);
|
|
||||||
const count = childCount.recordset[0].count;
|
|
||||||
|
|
||||||
// The child's sort is its 0-based position among the father's children.
|
// The child's sort is its 0-based position among the father's children.
|
||||||
await request(pool, tx)
|
await runner.query(queries.SET_FATHER_SORT, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('fatherArticleId', fatherId)
|
fatherArticleId: fatherId,
|
||||||
.input('fathersort', count - 1)
|
fathersort: count - 1,
|
||||||
.query(SET_FATHER_SORT);
|
});
|
||||||
|
|
||||||
await tx.commit();
|
const article = await load(articleId, languageId, runner);
|
||||||
} catch (err) {
|
if (!article) {
|
||||||
try {
|
throw new Error('Article not found');
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
return article;
|
||||||
if (!article) {
|
};
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return article;
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
CREATE as CREATE_PRICE,
|
CREATE as MSSQL_CREATE_PRICE,
|
||||||
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP,
|
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as MSSQL_DELETE_PRICE,
|
||||||
} from '../../providers/mssql/queries/price.js';
|
} from '../../providers/mssql/queries/price.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
CREATE as SQLITE_CREATE_PRICE,
|
||||||
|
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
|
||||||
|
} from '../../providers/sqlite3/queries/price.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function normalizePrices(prices) {
|
function normalizePrices(prices) {
|
||||||
if (!Array.isArray(prices)) {
|
if (!Array.isArray(prices)) {
|
||||||
@@ -42,10 +46,14 @@ function normalizePrices(prices) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function setPricesBySku(pool) {
|
export default function setPricesBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
|
||||||
|
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the price list of an article by its SKU.
|
* Replaces the price list of an article by its SKU.
|
||||||
@@ -53,6 +61,7 @@ export default function setPricesBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {Array<{price: number, tax: number, minmultiplier?: number}>} prices - The price entries (minmultiplier defaults to 1).
|
* @param {Array<{price: number, tax: number, minmultiplier?: number}>} prices - The price entries (minmultiplier defaults to 1).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If the prices are invalid or the article is not found.
|
* @throws {Error} If the prices are invalid or the article is not found.
|
||||||
*
|
*
|
||||||
@@ -60,58 +69,56 @@ export default function setPricesBySku(pool) {
|
|||||||
* const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
|
* const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, prices) => {
|
return async (googleAuthId, sku, prices, tx) => {
|
||||||
const normalized = normalizePrices(prices);
|
const normalized = normalizePrices(prices);
|
||||||
|
|
||||||
const { articleId, languageId, posCustomerGroupId } = await resolve(
|
const runSet = async (runner) => {
|
||||||
googleAuthId,
|
const { articleId, languageId, posCustomerGroupId } = await resolve(
|
||||||
sku,
|
googleAuthId,
|
||||||
);
|
sku,
|
||||||
if (!posCustomerGroupId) {
|
runner,
|
||||||
throw new Error('POS customer group required');
|
);
|
||||||
}
|
if (!posCustomerGroupId) {
|
||||||
|
throw new Error('POS customer group required');
|
||||||
|
}
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const taxIds = new Map();
|
const taxIds = new Map();
|
||||||
for (const entry of normalized) {
|
for (const entry of normalized) {
|
||||||
if (!taxIds.has(entry.tax)) {
|
if (!taxIds.has(entry.tax)) {
|
||||||
taxIds.set(entry.tax, await ensureTax(entry.tax, tx));
|
taxIds.set(entry.tax, await ensureTax(entry.tax, runner));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(deletePriceSql, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('customerGroupId', posCustomerGroupId)
|
customerGroupId: posCustomerGroupId,
|
||||||
.query(DELETE_BY_ARTICLE_AND_CUSTOMERGROUP);
|
});
|
||||||
|
|
||||||
for (const entry of normalized) {
|
for (const entry of normalized) {
|
||||||
await request(pool, tx)
|
await runner.query(createPriceSql, {
|
||||||
.input('taxId', taxIds.get(entry.tax))
|
taxId: taxIds.get(entry.tax),
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('minmultiplier', entry.minmultiplier)
|
minmultiplier: entry.minmultiplier,
|
||||||
.input('customerGroupId', posCustomerGroupId)
|
customerGroupId: posCustomerGroupId,
|
||||||
.input('net', entry.price)
|
net: entry.price,
|
||||||
.query(CREATE_PRICE);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
const article = await load(articleId, languageId, runner);
|
||||||
} catch (err) {
|
if (!article) {
|
||||||
try {
|
throw new Error('Article not found');
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
return article;
|
||||||
if (!article) {
|
};
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return article;
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
|
import { UPDATE_WEIGHT as MSSQL_UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
|
||||||
|
import { UPDATE_WEIGHT as SQLITE_UPDATE_WEIGHT } from '../../providers/sqlite3/queries/article.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function setWeightBySku(pool) {
|
export default function setWeightBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE_WEIGHT : MSSQL_UPDATE_WEIGHT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the weight of an article by its SKU.
|
* Sets the weight of an article by its SKU.
|
||||||
@@ -12,6 +15,7 @@ export default function setWeightBySku(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {number} weight - The weight (must be >= 0).
|
* @param {number} weight - The weight (must be >= 0).
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If the weight is invalid or the article is not found.
|
* @throws {Error} If the weight is invalid or the article is not found.
|
||||||
*
|
*
|
||||||
@@ -19,24 +23,24 @@ export default function setWeightBySku(pool) {
|
|||||||
* const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
|
* const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, weight) => {
|
return async (googleAuthId, sku, weight, tx) => {
|
||||||
if (!Number.isFinite(weight) || weight < 0) {
|
if (!Number.isFinite(weight) || weight < 0) {
|
||||||
throw new Error('Invalid weight');
|
throw new Error('Invalid weight');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, languageId } = await resolve(googleAuthId, sku);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { articleId, languageId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
articleId,
|
||||||
.input('articleId', articleId)
|
weight,
|
||||||
.input('weight', weight)
|
});
|
||||||
.query(UPDATE_WEIGHT);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
const article = await load(articleId, languageId, runner);
|
||||||
if (!article) {
|
if (!article) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
@@ -44,3 +48,4 @@ export default function setWeightBySku(pool) {
|
|||||||
return article;
|
return article;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import {
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
UPDATE_UNIT,
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
UPDATE_UNITAMOUNT,
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
UPDATE_UNIT_AND_AMOUNT,
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
} from '../../providers/mssql/queries/article.js';
|
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as MSSQL_UPDATE_TEXT } from '../../providers/mssql/queries/articletext.js';
|
||||||
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as SQLITE_UPDATE_TEXT } from '../../providers/sqlite3/queries/articletext.js';
|
||||||
import { UPDATE_BY_ARTICLE_AND_LANGUAGE } from '../../providers/mssql/queries/articletext.js';
|
import { GET_DETAIL_BY_ARTICLE_ID as MSSQL_GET_DETAIL } from '../../providers/mssql/queries/articleDetail.js';
|
||||||
import { GET_DETAIL_BY_ARTICLE_ID } from '../../providers/mssql/queries/articleDetail.js';
|
import { GET_DETAIL_BY_ARTICLE_ID as SQLITE_GET_DETAIL } from '../../providers/sqlite3/queries/articleDetail.js';
|
||||||
import { GET_FULL_BY_SI } from '../../providers/mssql/queries/unit.js';
|
import { GET_FULL_BY_SI as MSSQL_GET_UNIT } from '../../providers/mssql/queries/unit.js';
|
||||||
import request from '../request.js';
|
import { GET_FULL_BY_SI as SQLITE_GET_UNIT } from '../../providers/sqlite3/queries/unit.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
|
||||||
function formatUnitLabel(amount, symbol) {
|
function formatUnitLabel(amount, symbol) {
|
||||||
@@ -20,9 +21,16 @@ function formatUnitLabel(amount, symbol) {
|
|||||||
return `${Number(amount)}${sym}`;
|
return `${Number(amount)}${sym}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function updateBySku(pool) {
|
export default function updateBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(pool);
|
const load = queryBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
const updateTextSql = isSqlite ? SQLITE_UPDATE_TEXT : MSSQL_UPDATE_TEXT;
|
||||||
|
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
|
||||||
|
const getUnitSql = isSqlite ? SQLITE_GET_UNIT : MSSQL_GET_UNIT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an article's name and/or unit (SI and amount), recording history.
|
* Updates an article's name and/or unit (SI and amount), recording history.
|
||||||
@@ -33,6 +41,7 @@ export default function updateBySku(pool) {
|
|||||||
* @param {string} [patch.name] - The new article name.
|
* @param {string} [patch.name] - The new article name.
|
||||||
* @param {string} [patch.unitSi] - The new unit SI code.
|
* @param {string} [patch.unitSi] - The new unit SI code.
|
||||||
* @param {number} [patch.unitamount] - The new unit amount (must be > 0).
|
* @param {number} [patch.unitamount] - The new unit amount (must be > 0).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If no valid field is provided or the article is not found.
|
* @throws {Error} If no valid field is provided or the article is not found.
|
||||||
*
|
*
|
||||||
@@ -40,7 +49,7 @@ export default function updateBySku(pool) {
|
|||||||
* const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 });
|
* const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 });
|
||||||
* // => { id, sku, name, ... }
|
* // => { id, sku, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, patch = {}) => {
|
return async (googleAuthId, sku, patch = {}, tx) => {
|
||||||
const hasName = Object.prototype.hasOwnProperty.call(patch, 'name');
|
const hasName = Object.prototype.hasOwnProperty.call(patch, 'name');
|
||||||
const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount');
|
const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount');
|
||||||
const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi');
|
const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi');
|
||||||
@@ -57,122 +66,118 @@ export default function updateBySku(pool) {
|
|||||||
throw new Error('Invalid unitamount');
|
throw new Error('Invalid unitamount');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, languageId, companyId, companyuserId } = await resolve(
|
const runUpdate = async (runner) => {
|
||||||
googleAuthId,
|
const { articleId, languageId, companyId, companyuserId } = await resolve(
|
||||||
sku,
|
googleAuthId,
|
||||||
);
|
sku,
|
||||||
|
runner,
|
||||||
const currentResult = await pool
|
|
||||||
.request()
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('languageId', languageId)
|
|
||||||
.query(GET_DETAIL_BY_ARTICLE_ID);
|
|
||||||
const current = currentResult.recordset[0];
|
|
||||||
if (!current) {
|
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
let unitId;
|
|
||||||
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
|
|
||||||
if (hasUnitSi) {
|
|
||||||
if (!patch.unitSi?.trim()) {
|
|
||||||
throw new Error('Unit not found');
|
|
||||||
}
|
|
||||||
const unit = await pool
|
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.input('si', patch.unitSi.trim())
|
|
||||||
.input('languageId', languageId)
|
|
||||||
.query(GET_FULL_BY_SI);
|
|
||||||
const unitRow = unit.recordset[0];
|
|
||||||
unitId = unitRow?.id;
|
|
||||||
if (!unitId) {
|
|
||||||
throw new Error('Unit not found');
|
|
||||||
}
|
|
||||||
newUnitSymbol = unitRow.symbol ?? unitRow.si ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const historyLines = [];
|
|
||||||
if (hasName) {
|
|
||||||
const oldName = current.name ?? '';
|
|
||||||
const newName = patch.name.trim();
|
|
||||||
if (oldName !== newName) {
|
|
||||||
historyLines.push(`Name geändert von "${oldName}" auf "${newName}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasUnitSi || hasUnitamount) {
|
|
||||||
const oldLabel = formatUnitLabel(
|
|
||||||
current.unitamount,
|
|
||||||
current.unitSymbol ?? current.unitSi ?? '',
|
|
||||||
);
|
);
|
||||||
const newLabel = formatUnitLabel(
|
|
||||||
hasUnitamount ? patch.unitamount : current.unitamount,
|
const currentResult = await runner.query(getDetailSql, {
|
||||||
newUnitSymbol,
|
articleId,
|
||||||
);
|
languageId,
|
||||||
if (oldLabel !== newLabel) {
|
});
|
||||||
historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`);
|
const current = currentResult.rows[0];
|
||||||
|
if (!current) {
|
||||||
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
let unitId;
|
||||||
await tx.begin();
|
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
|
||||||
|
if (hasUnitSi) {
|
||||||
|
if (!patch.unitSi?.trim()) {
|
||||||
|
throw new Error('Unit not found');
|
||||||
|
}
|
||||||
|
const unit = await runner.query(getUnitSql, {
|
||||||
|
companyId,
|
||||||
|
si: patch.unitSi.trim(),
|
||||||
|
languageId,
|
||||||
|
});
|
||||||
|
const unitRow = unit.rows[0];
|
||||||
|
unitId = unitRow?.id;
|
||||||
|
if (!unitId) {
|
||||||
|
throw new Error('Unit not found');
|
||||||
|
}
|
||||||
|
newUnitSymbol = unitRow.symbol ?? unitRow.si ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyLines = [];
|
||||||
|
if (hasName) {
|
||||||
|
const oldName = current.name ?? '';
|
||||||
|
const newName = patch.name.trim();
|
||||||
|
if (oldName !== newName) {
|
||||||
|
historyLines.push(`Name geändert von "${oldName}" auf "${newName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasUnitSi || hasUnitamount) {
|
||||||
|
const oldLabel = formatUnitLabel(
|
||||||
|
current.unitamount,
|
||||||
|
current.unitSymbol ?? current.unitSi ?? '',
|
||||||
|
);
|
||||||
|
const newLabel = formatUnitLabel(
|
||||||
|
hasUnitamount ? patch.unitamount : current.unitamount,
|
||||||
|
newUnitSymbol,
|
||||||
|
);
|
||||||
|
if (oldLabel !== newLabel) {
|
||||||
|
historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
if (hasName) {
|
if (hasName) {
|
||||||
const slug = slugify(patch.name);
|
const slug = slugify(patch.name);
|
||||||
const update = await request(pool, tx)
|
const update = await runner.query(updateTextSql, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('name', patch.name.trim())
|
name: patch.name.trim(),
|
||||||
.input('slug', slug)
|
slug,
|
||||||
.query(UPDATE_BY_ARTICLE_AND_LANGUAGE);
|
});
|
||||||
|
|
||||||
if (!update.rowsAffected[0]) {
|
if (!update.rowsAffected) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUnitSi && hasUnitamount) {
|
if (hasUnitSi && hasUnitamount) {
|
||||||
await request(pool, tx)
|
await runner.query(articleQueries.UPDATE_UNIT_AND_AMOUNT, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('unitId', unitId)
|
unitId,
|
||||||
.input('unitamount', patch.unitamount)
|
unitamount: patch.unitamount,
|
||||||
.query(UPDATE_UNIT_AND_AMOUNT);
|
});
|
||||||
} else if (hasUnitSi) {
|
} else if (hasUnitSi) {
|
||||||
await request(pool, tx)
|
await runner.query(articleQueries.UPDATE_UNIT, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('unitId', unitId)
|
unitId,
|
||||||
.query(UPDATE_UNIT);
|
});
|
||||||
} else if (hasUnitamount) {
|
} else if (hasUnitamount) {
|
||||||
await request(pool, tx)
|
await runner.query(articleQueries.UPDATE_UNITAMOUNT, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('unitamount', patch.unitamount)
|
unitamount: patch.unitamount,
|
||||||
.query(UPDATE_UNITAMOUNT);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historyLines.length) {
|
if (historyLines.length) {
|
||||||
await request(pool, tx)
|
await runner.query(insertHistorySql, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('companyuserId', companyuserId)
|
companyuserId,
|
||||||
.input('text', historyLines.join('\n'))
|
text: historyLines.join('\n'),
|
||||||
.query(INSERT_HISTORY);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
const article = await load(articleId, languageId, runner);
|
||||||
} catch (err) {
|
if (!article) {
|
||||||
try {
|
throw new Error('Article not found');
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await load(articleId, languageId);
|
return article;
|
||||||
if (!article) {
|
};
|
||||||
throw new Error('Article not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return article;
|
if (tx) {
|
||||||
|
return runUpdate(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runUpdate);
|
||||||
|
}
|
||||||
|
return runUpdate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
CHECK_EXISTS,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
CREATE as CREATE_ARTICLE_SUPPLIER,
|
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
||||||
GET_BY_ID_AND_ARTICLE_ID,
|
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
|
||||||
GET_NEXT_SORT,
|
|
||||||
} from '../../providers/mssql/queries/articlesupplier.js';
|
|
||||||
import { CREATE as CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
|
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
@@ -15,10 +11,15 @@ import {
|
|||||||
resolveCompanyDefaultTax,
|
resolveCompanyDefaultTax,
|
||||||
shapeArticleSupplier,
|
shapeArticleSupplier,
|
||||||
} from './helpers.js';
|
} from './helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigns a supplier to an article, optionally creating a purchase price.
|
* Assigns a supplier to an article, optionally creating a purchase price.
|
||||||
@@ -34,6 +35,7 @@ export default function create(pool) {
|
|||||||
* @param {number|null} [input.netPrice] - Net purchase price.
|
* @param {number|null} [input.netPrice] - Net purchase price.
|
||||||
* @param {number} [input.taxId] - The tax id for the purchase price.
|
* @param {number} [input.taxId] - The tax id for the purchase price.
|
||||||
* @param {number} [input.taxRate] - The tax rate in percent (used if taxId is not provided).
|
* @param {number} [input.taxRate] - The tax rate in percent (used if taxId is not provided).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The created assignment.
|
* @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The created assignment.
|
||||||
* @throws {Error} If input is invalid, article or supplier is not found, or supplier is already assigned.
|
* @throws {Error} If input is invalid, article or supplier is not found, or supplier is already assigned.
|
||||||
*
|
*
|
||||||
@@ -46,7 +48,7 @@ export default function create(pool) {
|
|||||||
* });
|
* });
|
||||||
* // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
* // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, input = {}) => {
|
return async (googleAuthId, sku, input = {}, tx) => {
|
||||||
const entityId = input.entityId;
|
const entityId = input.entityId;
|
||||||
if (entityId == null || !Number.isFinite(entityId)) {
|
if (entityId == null || !Number.isFinite(entityId)) {
|
||||||
throw new Error('Supplier required');
|
throw new Error('Supplier required');
|
||||||
@@ -79,30 +81,26 @@ export default function create(pool) {
|
|||||||
throw new Error('Invalid price');
|
throw new Error('Invalid price');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, companyId } = await resolve(googleAuthId, sku);
|
const runCreate = async (runner) => {
|
||||||
|
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
await assertSupplier(pool, null, companyId, entityId);
|
await assertSupplier(runner, companyId, entityId);
|
||||||
|
|
||||||
const existingCheck = await pool
|
const existingCheck = await runner.query(queries.CHECK_EXISTS, {
|
||||||
.request()
|
articleId,
|
||||||
.input('articleId', articleId)
|
entityId,
|
||||||
.input('entityId', entityId)
|
});
|
||||||
.query(CHECK_EXISTS);
|
|
||||||
|
|
||||||
if (existingCheck.recordset[0]) {
|
if (existingCheck.rows[0]) {
|
||||||
throw new Error('Supplier already assigned');
|
throw new Error('Supplier already assigned');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
let sort = input.sort;
|
let sort = input.sort;
|
||||||
if (sort === undefined || sort === null) {
|
if (sort === undefined || sort === null) {
|
||||||
const sortRes = await request(pool, tx)
|
const sortRes = await runner.query(queries.GET_NEXT_SORT, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.query(GET_NEXT_SORT);
|
});
|
||||||
sort = sortRes.recordset[0].sort;
|
sort = sortRes.rows[0].sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
let priceId = null;
|
let priceId = null;
|
||||||
@@ -111,63 +109,60 @@ export default function create(pool) {
|
|||||||
let taxId = input.taxId;
|
let taxId = input.taxId;
|
||||||
if (taxId == null || !Number.isFinite(taxId)) {
|
if (taxId == null || !Number.isFinite(taxId)) {
|
||||||
if (input.taxRate != null && Number.isFinite(input.taxRate)) {
|
if (input.taxRate != null && Number.isFinite(input.taxRate)) {
|
||||||
taxId = await ensureTax(input.taxRate, tx);
|
taxId = await ensureTax(input.taxRate, runner);
|
||||||
} else {
|
} else {
|
||||||
const defaultTaxId = await resolveCompanyDefaultTax(
|
const defaultTaxId = await resolveCompanyDefaultTax(
|
||||||
pool,
|
runner,
|
||||||
tx,
|
|
||||||
companyId,
|
companyId,
|
||||||
);
|
);
|
||||||
taxId = defaultTaxId != null ? defaultTaxId : await ensureTax(0, tx);
|
taxId = defaultTaxId != null ? defaultTaxId : await ensureTax(0, runner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerGroupId = await ensureLieferantenGroup(
|
const customerGroupId = await ensureLieferantenGroup(
|
||||||
pool,
|
runner,
|
||||||
companyId,
|
companyId,
|
||||||
tx,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const priceRes = await request(pool, tx)
|
const priceRes = await runner.query(createPriceSql, {
|
||||||
.input('taxId', taxId)
|
taxId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('minmultiplier', 1)
|
minmultiplier: 1,
|
||||||
.input('customerGroupId', customerGroupId)
|
customerGroupId,
|
||||||
.input('net', netPrice)
|
net: netPrice,
|
||||||
.query(CREATE_PRICE);
|
});
|
||||||
|
|
||||||
priceId = priceRes.recordset[0].id;
|
priceId = priceRes.rows[0]?.id ?? priceRes.insertId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdRes = await request(pool, tx)
|
const createdRes = await runner.query(queries.CREATE, {
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('entityId', entityId)
|
entityId,
|
||||||
.input('priceId', priceId)
|
priceId,
|
||||||
.input('sku', supplierSku)
|
sku: supplierSku,
|
||||||
.input('stock', stock)
|
stock,
|
||||||
.input('pu', pu)
|
pu,
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(CREATE_ARTICLE_SUPPLIER);
|
});
|
||||||
|
|
||||||
const createdId = createdRes.recordset[0].id;
|
const createdId = createdRes.rows[0]?.id ?? createdRes.insertId;
|
||||||
|
|
||||||
await tx.commit();
|
const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
|
||||||
|
id: createdId,
|
||||||
|
articleId,
|
||||||
|
companyId,
|
||||||
|
});
|
||||||
|
|
||||||
const loadRes = await pool
|
return shapeArticleSupplier(loadRes.rows[0]);
|
||||||
.request()
|
};
|
||||||
.input('id', createdId)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_BY_ID_AND_ARTICLE_ID);
|
|
||||||
|
|
||||||
return shapeArticleSupplier(loadRes.recordset[0]);
|
if (tx) {
|
||||||
} catch (err) {
|
return runCreate(makeRunner(provider, tx));
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
DELETE_BY_ID_AND_ARTICLE_ID,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
DELETE_PRICE,
|
|
||||||
GET_BY_ID_AND_ARTICLE_ID,
|
|
||||||
} from '../../providers/mssql/queries/articlesupplier.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a supplier assignment and cleans up its associated purchase price.
|
* Deletes a supplier assignment and cleans up its associated purchase price.
|
||||||
@@ -15,55 +14,52 @@ export default function del(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {number} id - The id of the articlesupplier record.
|
* @param {number} id - The id of the articlesupplier record.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the supplier assignment is not found.
|
* @throws {Error} If the supplier assignment is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.articlesupplier.delete(42, 'SKU-1', 12);
|
* await store.articlesupplier.delete(42, 'SKU-1', 12);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, id) => {
|
return async (googleAuthId, sku, id, tx) => {
|
||||||
if (id == null || !Number.isFinite(id)) {
|
if (id == null || !Number.isFinite(id)) {
|
||||||
throw new Error('Supplier assignment not found');
|
throw new Error('Supplier assignment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, companyId } = await resolve(googleAuthId, sku);
|
const runDelete = async (runner) => {
|
||||||
|
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const existingRes = await pool
|
const existingRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
articleId,
|
||||||
.input('articleId', articleId)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(GET_BY_ID_AND_ARTICLE_ID);
|
|
||||||
|
|
||||||
const existing = existingRes.recordset[0];
|
const existing = existingRes.rows[0];
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new Error('Supplier assignment not found');
|
throw new Error('Supplier assignment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = pool.transaction();
|
await runner.query(queries.DELETE_BY_ID_AND_ARTICLE_ID, {
|
||||||
await tx.begin();
|
id,
|
||||||
|
articleId,
|
||||||
try {
|
});
|
||||||
await request(pool, tx)
|
|
||||||
.input('id', id)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.query(DELETE_BY_ID_AND_ARTICLE_ID);
|
|
||||||
|
|
||||||
if (existing.priceId != null) {
|
if (existing.priceId != null) {
|
||||||
await request(pool, tx)
|
await runner.query(queries.DELETE_PRICE, {
|
||||||
.input('id', existing.priceId)
|
id: existing.priceId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.query(DELETE_PRICE);
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await tx.commit();
|
if (tx) {
|
||||||
} catch (err) {
|
return runDelete(makeRunner(provider, tx));
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runDelete);
|
||||||
|
}
|
||||||
|
return runDelete(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { GET_SUPPLIER_BY_ID, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
import {
|
||||||
import { GET_COMPANY_DEFAULT_TAX } from '../../providers/mssql/queries/articlesupplier.js';
|
GET_SUPPLIER_BY_ID as MSSQL_GET_SUPPLIER_BY_ID,
|
||||||
import request from '../request.js';
|
SUPPLIER_GROUP_NAME as MSSQL_SUPPLIER_GROUP_NAME,
|
||||||
|
} from '../../providers/mssql/queries/entity.js';
|
||||||
|
import {
|
||||||
|
GET_SUPPLIER_BY_ID as SQLITE_GET_SUPPLIER_BY_ID,
|
||||||
|
SUPPLIER_GROUP_NAME as SQLITE_SUPPLIER_GROUP_NAME,
|
||||||
|
} from '../../providers/sqlite3/queries/entity.js';
|
||||||
|
import { GET_COMPANY_DEFAULT_TAX as MSSQL_DEFAULT_TAX } from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
|
import { GET_COMPANY_DEFAULT_TAX as SQLITE_DEFAULT_TAX } from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export function optionalString(value) {
|
export function optionalString(value) {
|
||||||
if (value == null || value === '') return null;
|
if (value == null || value === '') return null;
|
||||||
@@ -30,26 +38,58 @@ export function shapeArticleSupplier(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function assertSupplier(pool, tx, companyId, supplierId) {
|
export async function assertSupplier(runnerOrPool, txOrCompanyId, maybeCompanyId, maybeSupplierId) {
|
||||||
|
let runner;
|
||||||
|
let companyId;
|
||||||
|
let supplierId;
|
||||||
|
|
||||||
|
if (maybeSupplierId !== undefined) {
|
||||||
|
runner = makeRunner(runnerOrPool, txOrCompanyId);
|
||||||
|
companyId = maybeCompanyId;
|
||||||
|
supplierId = maybeSupplierId;
|
||||||
|
} else {
|
||||||
|
runner = makeRunner(runnerOrPool);
|
||||||
|
companyId = txOrCompanyId;
|
||||||
|
supplierId = maybeCompanyId;
|
||||||
|
}
|
||||||
|
|
||||||
if (supplierId == null || !Number.isFinite(supplierId)) {
|
if (supplierId == null || !Number.isFinite(supplierId)) {
|
||||||
throw new Error('Supplier not found');
|
throw new Error('Supplier not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
const isSqlite = runner?.dialect === 'sqlite3';
|
||||||
.input('id', supplierId)
|
const getSupplierSql = isSqlite ? SQLITE_GET_SUPPLIER_BY_ID : MSSQL_GET_SUPPLIER_BY_ID;
|
||||||
.input('companyId', companyId)
|
const groupName = isSqlite ? SQLITE_SUPPLIER_GROUP_NAME : MSSQL_SUPPLIER_GROUP_NAME;
|
||||||
.input('groupName', SUPPLIER_GROUP_NAME)
|
|
||||||
.query(GET_SUPPLIER_BY_ID);
|
|
||||||
|
|
||||||
if (!result.recordset[0]) {
|
const result = await runner.query(getSupplierSql, {
|
||||||
|
id: supplierId,
|
||||||
|
companyId,
|
||||||
|
groupName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.rows[0]) {
|
||||||
throw new Error('Supplier not found');
|
throw new Error('Supplier not found');
|
||||||
}
|
}
|
||||||
return supplierId;
|
return supplierId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveCompanyDefaultTax(pool, tx, companyId) {
|
export async function resolveCompanyDefaultTax(runnerOrPool, txOrCompanyId, maybeCompanyId) {
|
||||||
const result = await request(pool, tx)
|
let runner;
|
||||||
.input('companyId', companyId)
|
let companyId;
|
||||||
.query(GET_COMPANY_DEFAULT_TAX);
|
|
||||||
return result.recordset[0]?.taxrate ?? null;
|
if (maybeCompanyId !== undefined) {
|
||||||
|
runner = makeRunner(runnerOrPool, txOrCompanyId);
|
||||||
|
companyId = maybeCompanyId;
|
||||||
|
} else {
|
||||||
|
runner = makeRunner(runnerOrPool);
|
||||||
|
companyId = txOrCompanyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSqlite = runner?.dialect === 'sqlite3';
|
||||||
|
const defaultTaxSql = isSqlite ? SQLITE_DEFAULT_TAX : MSSQL_DEFAULT_TAX;
|
||||||
|
|
||||||
|
const result = await runner.query(defaultTaxSql, {
|
||||||
|
companyId,
|
||||||
|
});
|
||||||
|
return result.rows[0]?.taxrate ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { LIST_BY_ARTICLE_ID } from '../../providers/mssql/queries/articlesupplier.js';
|
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
|
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
import { shapeArticleSupplier } from './helpers.js';
|
import { shapeArticleSupplier } from './helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function listByArticleSku(pool) {
|
export default function listByArticleSku(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the supplier assignments for an article by its SKU.
|
* Lists the supplier assignments for an article by its SKU.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>>} The supplier assignments.
|
* @returns {Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>>} The supplier assignments.
|
||||||
* @throws {Error} If the article is not found or the user is not authorized.
|
* @throws {Error} If the article is not found or the user is not authorized.
|
||||||
*
|
*
|
||||||
@@ -17,15 +21,16 @@ export default function listByArticleSku(pool) {
|
|||||||
* const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1');
|
* const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1');
|
||||||
* // => [{ id, articleId, entityId, supplierName, sku, pu, stock, priceId, price }]
|
* // => [{ id, articleId, entityId, supplierName, sku, pu, stock, priceId, price }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku) => {
|
return async (googleAuthId, sku, tx) => {
|
||||||
const { articleId, companyId } = await resolve(googleAuthId, sku);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
articleId,
|
||||||
.input('articleId', articleId)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(LIST_BY_ARTICLE_ID);
|
|
||||||
|
|
||||||
return result.recordset.map(shapeArticleSupplier);
|
return result.rows.map(shapeArticleSupplier);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { LIST_BY_ENTITY_AND_SKUS } from '../../providers/mssql/queries/articlesupplier.js';
|
import { LIST_BY_ENTITY_AND_SKUS as MSSQL_QUERY } from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
|
import { LIST_BY_ENTITY_AND_SKUS as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import { assertSupplier } from './helpers.js';
|
import { assertSupplier } from './helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function parseJsonArray(raw) {
|
function parseJsonArray(raw) {
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
@@ -12,8 +14,9 @@ function parseJsonArray(raw) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function listByEntityAndSkus(pool) {
|
export default function listByEntityAndSkus(provider) {
|
||||||
const resolveAccess = resolveSelectedCompanyAccess(pool);
|
const resolveAccess = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists article-supplier assignments for a given supplier (entityId) and SKU list,
|
* Lists article-supplier assignments for a given supplier (entityId) and SKU list,
|
||||||
@@ -22,6 +25,7 @@ export default function listByEntityAndSkus(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number|{entityId?: number, supplierId?: number, skus?: string[]|string}} entityIdOrOptions - The supplier entity id or an options object.
|
* @param {number|{entityId?: number, supplierId?: number, skus?: string[]|string}} entityIdOrOptions - The supplier entity id or an options object.
|
||||||
* @param {string[]|string} [maybeSkus] - The list of SKUs to filter by (or single SKU string).
|
* @param {string[]|string} [maybeSkus] - The list of SKUs to filter by (or single SKU string).
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>} The supplier assignments with article details.
|
* @returns {Promise<Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>} The supplier assignments with article details.
|
||||||
* @throws {Error} If user is not authorized, entityId is missing, or supplier does not exist.
|
* @throws {Error} If user is not authorized, entityId is missing, or supplier does not exist.
|
||||||
*
|
*
|
||||||
@@ -29,7 +33,7 @@ export default function listByEntityAndSkus(pool) {
|
|||||||
* const items = await store.articlesupplier.listByEntityAndSkus(42, 5, ['SKU-1', 'SKU-2']);
|
* const items = await store.articlesupplier.listByEntityAndSkus(42, 5, ['SKU-1', 'SKU-2']);
|
||||||
* // => [{ id, articleId, entityId, supplierName, sku, pu, stock, sort, price, article: { id, sku, name, slug, photos, eans } }]
|
* // => [{ id, articleId, entityId, supplierName, sku, pu, stock, sort, price, article: { id, sku, name, slug, photos, eans } }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityIdOrOptions, maybeSkus) => {
|
return async (googleAuthId, entityIdOrOptions, maybeSkus, tx) => {
|
||||||
let entityId;
|
let entityId;
|
||||||
let skus;
|
let skus;
|
||||||
|
|
||||||
@@ -68,19 +72,19 @@ export default function listByEntityAndSkus(pool) {
|
|||||||
skusJson = JSON.stringify(Array.from(new Set(cleanList)));
|
skusJson = JSON.stringify(Array.from(new Set(cleanList)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId, languageId } = await resolveAccess(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId, languageId } = await resolveAccess(googleAuthId, runner);
|
||||||
|
|
||||||
await assertSupplier(pool, null, companyId, parsedEntityId);
|
await assertSupplier(runner, companyId, parsedEntityId);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
entityId: parsedEntityId,
|
||||||
.input('entityId', parsedEntityId)
|
companyId,
|
||||||
.input('companyId', companyId)
|
languageId,
|
||||||
.input('languageId', languageId)
|
skusJson,
|
||||||
.input('skusJson', skusJson)
|
});
|
||||||
.query(LIST_BY_ENTITY_AND_SKUS);
|
|
||||||
|
|
||||||
return result.recordset.map((row) => {
|
return result.rows.map((row) => {
|
||||||
const rawPhotos = parseJsonArray(row.photosJson);
|
const rawPhotos = parseJsonArray(row.photosJson);
|
||||||
const photos = rawPhotos.map((p) => ({
|
const photos = rawPhotos.map((p) => ({
|
||||||
sort: p.sort,
|
sort: p.sort,
|
||||||
@@ -128,3 +132,4 @@ export default function listByEntityAndSkus(pool) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
LIST_IDS_BY_ARTICLE,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
SET_TEMP_SORTS,
|
|
||||||
UPDATE_SORT,
|
|
||||||
} from '../../providers/mssql/queries/articlesupplier.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function order(pool) {
|
export default function order(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the supplier assignments of an article by its SKU.
|
* Reorders the supplier assignments of an article by its SKU.
|
||||||
@@ -16,6 +15,7 @@ export default function order(pool) {
|
|||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
* @param {object} input - The reorder input.
|
* @param {object} input - The reorder input.
|
||||||
* @param {Array<number>} input.ids - The articlesupplier ids in their new order.
|
* @param {Array<number>} input.ids - The articlesupplier ids in their new order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{ids: Array<number>}>} The applied order.
|
* @returns {Promise<{ids: Array<number>}>} The applied order.
|
||||||
* @throws {Error} If the order is invalid.
|
* @throws {Error} If the order is invalid.
|
||||||
*
|
*
|
||||||
@@ -23,62 +23,58 @@ export default function order(pool) {
|
|||||||
* const result = await store.articlesupplier.order(42, 'SKU-1', { ids: [12, 10, 15] });
|
* const result = await store.articlesupplier.order(42, 'SKU-1', { ids: [12, 10, 15] });
|
||||||
* // => { ids: [12, 10, 15] }
|
* // => { ids: [12, 10, 15] }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, { ids } = {}) => {
|
return async (googleAuthId, sku, { ids } = {}, tx) => {
|
||||||
if (!Array.isArray(ids)) {
|
if (!Array.isArray(ids)) {
|
||||||
throw new Error('Invalid supplier order');
|
throw new Error('Invalid supplier order');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId } = await resolve(googleAuthId, sku);
|
const runOrder = async (runner) => {
|
||||||
|
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const current = await pool
|
const current = await runner.query(queries.LIST_IDS_BY_ARTICLE, {
|
||||||
.request()
|
articleId,
|
||||||
.input('articleId', articleId)
|
});
|
||||||
.query(LIST_IDS_BY_ARTICLE);
|
|
||||||
|
|
||||||
const existing = new Set(current.recordset.map((r) => r.id));
|
const existing = new Set(current.rows.map((r) => r.id));
|
||||||
|
|
||||||
if (ids.length !== existing.size) {
|
if (ids.length !== existing.size) {
|
||||||
throw new Error('Invalid supplier order');
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
for (const id of ids) {
|
|
||||||
if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) {
|
|
||||||
throw new Error('Invalid supplier order');
|
throw new Error('Invalid supplier order');
|
||||||
}
|
}
|
||||||
seen.add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const seen = new Set();
|
||||||
await tx.begin();
|
for (const id of ids) {
|
||||||
|
if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) {
|
||||||
|
throw new Error('Invalid supplier order');
|
||||||
|
}
|
||||||
|
seen.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
await runner.query(queries.SET_TEMP_SORTS, {
|
||||||
await request(pool, tx)
|
articleId,
|
||||||
.input('articleId', articleId)
|
});
|
||||||
.query(SET_TEMP_SORTS);
|
|
||||||
|
|
||||||
for (let sort = 0; sort < ids.length; sort++) {
|
for (let sort = 0; sort < ids.length; sort++) {
|
||||||
const updateRes = await request(pool, tx)
|
const updateRes = await runner.query(queries.UPDATE_SORT, {
|
||||||
.input('id', ids[sort])
|
id: ids[sort],
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(UPDATE_SORT);
|
});
|
||||||
|
|
||||||
if (!updateRes.rowsAffected[0]) {
|
if (!updateRes.rowsAffected) {
|
||||||
throw new Error('Invalid supplier order');
|
throw new Error('Invalid supplier order');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return { ids };
|
return { ids };
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runOrder(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runOrder);
|
||||||
|
}
|
||||||
|
return runOrder(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
|
||||||
DELETE_PRICE,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
|
||||||
GET_BY_ID_AND_ARTICLE_ID,
|
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
||||||
UPDATE as UPDATE_ARTICLE_SUPPLIER,
|
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
|
||||||
UPDATE_PRICE,
|
|
||||||
} from '../../providers/mssql/queries/articlesupplier.js';
|
|
||||||
import { CREATE as CREATE_PRICE } from '../../providers/mssql/queries/price.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
|
import ensureLieferantenGroup from '../supplier/ensureLieferantenGroup.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
@@ -14,10 +10,15 @@ import {
|
|||||||
resolveCompanyDefaultTax,
|
resolveCompanyDefaultTax,
|
||||||
shapeArticleSupplier,
|
shapeArticleSupplier,
|
||||||
} from './helpers.js';
|
} from './helpers.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const resolve = resolveArticleBySku(pool);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing supplier assignment and its associated purchase price.
|
* Updates an existing supplier assignment and its associated purchase price.
|
||||||
@@ -33,6 +34,7 @@ export default function update(pool) {
|
|||||||
* @param {number|null} [patch.netPrice] - Net purchase price.
|
* @param {number|null} [patch.netPrice] - Net purchase price.
|
||||||
* @param {number} [patch.taxId] - The tax id for the purchase price.
|
* @param {number} [patch.taxId] - The tax id for the purchase price.
|
||||||
* @param {number} [patch.taxRate] - The tax rate in percent.
|
* @param {number} [patch.taxRate] - The tax rate in percent.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The updated assignment.
|
* @returns {Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>} The updated assignment.
|
||||||
* @throws {Error} If input is invalid or supplier assignment is not found.
|
* @throws {Error} If input is invalid or supplier assignment is not found.
|
||||||
*
|
*
|
||||||
@@ -44,7 +46,7 @@ export default function update(pool) {
|
|||||||
* });
|
* });
|
||||||
* // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
* // => { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, id, patch = {}) => {
|
return async (googleAuthId, sku, id, patch = {}, tx) => {
|
||||||
if (id == null || !Number.isFinite(id)) {
|
if (id == null || !Number.isFinite(id)) {
|
||||||
throw new Error('Supplier assignment not found');
|
throw new Error('Supplier assignment not found');
|
||||||
}
|
}
|
||||||
@@ -73,24 +75,20 @@ export default function update(pool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { articleId, companyId } = await resolve(googleAuthId, sku);
|
const runUpdate = async (runner) => {
|
||||||
|
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
const existingRes = await pool
|
const existingRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
articleId,
|
||||||
.input('articleId', articleId)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(GET_BY_ID_AND_ARTICLE_ID);
|
|
||||||
|
|
||||||
const existing = existingRes.recordset[0];
|
const existing = existingRes.rows[0];
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new Error('Supplier assignment not found');
|
throw new Error('Supplier assignment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
let priceId = existing.priceId;
|
let priceId = existing.priceId;
|
||||||
|
|
||||||
if (patch.netPrice !== undefined) {
|
if (patch.netPrice !== undefined) {
|
||||||
@@ -99,20 +97,20 @@ export default function update(pool) {
|
|||||||
const oldPriceId = priceId;
|
const oldPriceId = priceId;
|
||||||
priceId = null;
|
priceId = null;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(queries.UPDATE, {
|
||||||
.input('id', id)
|
id,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('sku', existing.sku)
|
sku: existing.sku,
|
||||||
.input('stock', existing.stock)
|
stock: existing.stock,
|
||||||
.input('pu', existing.pu)
|
pu: existing.pu,
|
||||||
.input('sort', existing.sort)
|
sort: existing.sort,
|
||||||
.input('priceId', null)
|
priceId: null,
|
||||||
.query(UPDATE_ARTICLE_SUPPLIER);
|
});
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(queries.DELETE_PRICE, {
|
||||||
.input('id', oldPriceId)
|
id: oldPriceId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.query(DELETE_PRICE);
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// netPrice is a valid number
|
// netPrice is a valid number
|
||||||
@@ -120,49 +118,47 @@ export default function update(pool) {
|
|||||||
let taxId = patch.taxId;
|
let taxId = patch.taxId;
|
||||||
if (taxId == null || !Number.isFinite(taxId)) {
|
if (taxId == null || !Number.isFinite(taxId)) {
|
||||||
if (patch.taxRate != null && Number.isFinite(patch.taxRate)) {
|
if (patch.taxRate != null && Number.isFinite(patch.taxRate)) {
|
||||||
taxId = await ensureTax(patch.taxRate, tx);
|
taxId = await ensureTax(patch.taxRate, runner);
|
||||||
} else {
|
} else {
|
||||||
taxId = existing.priceTaxId;
|
taxId = existing.priceTaxId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(queries.UPDATE_PRICE, {
|
||||||
.input('id', priceId)
|
id: priceId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('net', patch.netPrice)
|
net: patch.netPrice,
|
||||||
.input('taxId', taxId)
|
taxId,
|
||||||
.query(UPDATE_PRICE);
|
});
|
||||||
} else {
|
} else {
|
||||||
// create new price row
|
// create new price row
|
||||||
let taxId = patch.taxId;
|
let taxId = patch.taxId;
|
||||||
if (taxId == null || !Number.isFinite(taxId)) {
|
if (taxId == null || !Number.isFinite(taxId)) {
|
||||||
if (patch.taxRate != null && Number.isFinite(patch.taxRate)) {
|
if (patch.taxRate != null && Number.isFinite(patch.taxRate)) {
|
||||||
taxId = await ensureTax(patch.taxRate, tx);
|
taxId = await ensureTax(patch.taxRate, runner);
|
||||||
} else {
|
} else {
|
||||||
const defaultTaxId = await resolveCompanyDefaultTax(
|
const defaultTaxId = await resolveCompanyDefaultTax(
|
||||||
pool,
|
runner,
|
||||||
tx,
|
|
||||||
companyId,
|
companyId,
|
||||||
);
|
);
|
||||||
taxId = defaultTaxId != null ? defaultTaxId : await ensureTax(0, tx);
|
taxId = defaultTaxId != null ? defaultTaxId : await ensureTax(0, runner);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerGroupId = await ensureLieferantenGroup(
|
const customerGroupId = await ensureLieferantenGroup(
|
||||||
pool,
|
runner,
|
||||||
companyId,
|
companyId,
|
||||||
tx,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const priceRes = await request(pool, tx)
|
const priceRes = await runner.query(createPriceSql, {
|
||||||
.input('taxId', taxId)
|
taxId,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('minmultiplier', 1)
|
minmultiplier: 1,
|
||||||
.input('customerGroupId', customerGroupId)
|
customerGroupId,
|
||||||
.input('net', patch.netPrice)
|
net: patch.netPrice,
|
||||||
.query(CREATE_PRICE);
|
});
|
||||||
|
|
||||||
priceId = priceRes.recordset[0].id;
|
priceId = priceRes.rows[0]?.id ?? priceRes.insertId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,33 +174,32 @@ export default function update(pool) {
|
|||||||
const nextPu = patch.pu !== undefined ? patch.pu : existing.pu;
|
const nextPu = patch.pu !== undefined ? patch.pu : existing.pu;
|
||||||
const nextSort = patch.sort !== undefined ? patch.sort : existing.sort;
|
const nextSort = patch.sort !== undefined ? patch.sort : existing.sort;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(queries.UPDATE, {
|
||||||
.input('id', id)
|
id,
|
||||||
.input('articleId', articleId)
|
articleId,
|
||||||
.input('sku', nextSku)
|
sku: nextSku,
|
||||||
.input('stock', nextStock)
|
stock: nextStock,
|
||||||
.input('pu', nextPu)
|
pu: nextPu,
|
||||||
.input('sort', nextSort)
|
sort: nextSort,
|
||||||
.input('priceId', priceId)
|
priceId,
|
||||||
.query(UPDATE_ARTICLE_SUPPLIER);
|
});
|
||||||
|
|
||||||
await tx.commit();
|
const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
|
||||||
|
id,
|
||||||
|
articleId,
|
||||||
|
companyId,
|
||||||
|
});
|
||||||
|
|
||||||
const loadRes = await pool
|
return shapeArticleSupplier(loadRes.rows[0]);
|
||||||
.request()
|
};
|
||||||
.input('id', id)
|
|
||||||
.input('articleId', articleId)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_BY_ID_AND_ARTICLE_ID);
|
|
||||||
|
|
||||||
return shapeArticleSupplier(loadRes.recordset[0]);
|
if (tx) {
|
||||||
} catch (err) {
|
return runUpdate(makeRunner(provider, tx));
|
||||||
try {
|
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runUpdate);
|
||||||
|
}
|
||||||
|
return runUpdate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
import { DELETE_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/brand.js';
|
import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/brand.js';
|
||||||
|
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/brand.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a brand for the user's selected company by its id.
|
* Deletes a brand for the user's selected company by its id.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The id of the brand to delete.
|
* @param {number} id - The id of the brand to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the brand is not found.
|
* @throws {Error} If the brand is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.brand.delete(42, 3);
|
* await store.brand.delete(42, 3);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Brand not found');
|
throw new Error('Brand not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(DELETE_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Brand not found');
|
throw new Error('Brand not found');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.number === 547) {
|
if (isForeignKeyConstraintError(err)) {
|
||||||
throw new Error('Brand in use');
|
throw new Error('Brand in use');
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
import { LIST_BY_COMPANY } from '../../providers/mssql/queries/brand.js';
|
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 resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_LIST_BY_COMPANY
|
||||||
|
: MSSQL_LIST_BY_COMPANY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the brands of the user's selected company.
|
* Lists the brands of the user's selected company.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{id: number, name: string, gsprpinfo: string|null}>>} The brands.
|
* @returns {Promise<Array<{id: number, name: string, gsprpinfo: string|null}>>} The brands.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const brands = await store.brand.list(42);
|
* const brands = await store.brand.list(42);
|
||||||
* // => [{ id, name, gsprpinfo }]
|
* // => [{ id, name, gsprpinfo }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, { companyId });
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(LIST_BY_COMPANY);
|
|
||||||
|
|
||||||
return result.recordset;
|
return result.rows;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
CREATE,
|
CREATE as MSSQL_CREATE,
|
||||||
GET_BY_COMPANY_AND_NAME,
|
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BY_COMPANY_AND_NAME,
|
||||||
UPDATE_BY_ID,
|
UPDATE_BY_ID as MSSQL_UPDATE_BY_ID,
|
||||||
} from '../../providers/mssql/queries/brand.js';
|
} from '../../providers/mssql/queries/brand.js';
|
||||||
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_BY_COMPANY_AND_NAME as SQLITE_GET_BY_COMPANY_AND_NAME,
|
||||||
|
UPDATE_BY_ID as SQLITE_UPDATE_BY_ID,
|
||||||
|
} from '../../providers/sqlite3/queries/brand.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function upsert(pool) {
|
export default function upsert(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_NAME
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_NAME;
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE_BY_ID : MSSQL_UPDATE_BY_ID;
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or updates a brand for the user's selected company, keyed by name.
|
* Creates or updates a brand for the user's selected company, keyed by name.
|
||||||
@@ -15,6 +27,7 @@ export default function upsert(pool) {
|
|||||||
* @param {object} input - The brand input.
|
* @param {object} input - The brand input.
|
||||||
* @param {string} input.name - The brand name.
|
* @param {string} input.name - The brand name.
|
||||||
* @param {string} [input.gsprpinfo] - Optional GSPRP info.
|
* @param {string} [input.gsprpinfo] - Optional GSPRP info.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null}>} The upserted brand.
|
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null}>} The upserted brand.
|
||||||
* @throws {Error} If the brand is invalid.
|
* @throws {Error} If the brand is invalid.
|
||||||
*
|
*
|
||||||
@@ -22,42 +35,42 @@ export default function upsert(pool) {
|
|||||||
* const brand = await store.brand.upsert(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
|
* const brand = await store.brand.upsert(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
|
||||||
* // => { id, name, gsprpinfo }
|
* // => { id, name, gsprpinfo }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, input = {}) => {
|
return async (googleAuthId, input = {}, tx) => {
|
||||||
const name = input.name?.trim();
|
const name = input.name?.trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('Invalid brand');
|
throw new Error('Invalid brand');
|
||||||
}
|
}
|
||||||
const gsprpinfo = input.gsprpinfo ?? null;
|
const gsprpinfo = input.gsprpinfo ?? null;
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const existing = await pool
|
const existing = await runner.query(getSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
name,
|
||||||
.input('name', name)
|
});
|
||||||
.query(GET_BY_COMPANY_AND_NAME);
|
|
||||||
|
|
||||||
if (existing.recordset[0]) {
|
if (existing.rows[0]) {
|
||||||
const { id } = existing.recordset[0];
|
const { id } = existing.rows[0];
|
||||||
await pool
|
await runner.query(updateSql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
name,
|
||||||
.input('name', name)
|
gsprpinfo,
|
||||||
.input('gsprpinfo', gsprpinfo)
|
});
|
||||||
.query(UPDATE_BY_ID);
|
|
||||||
return { id, name, gsprpinfo };
|
return { id, name, gsprpinfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await pool
|
const created = await runner.query(createSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
name,
|
||||||
.input('name', name)
|
gsprpinfo,
|
||||||
.input('gsprpinfo', gsprpinfo)
|
});
|
||||||
.query(CREATE);
|
|
||||||
|
const id = created.rows[0]?.id ?? created.insertId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: created.recordset[0].id,
|
id,
|
||||||
name,
|
name,
|
||||||
gsprpinfo,
|
gsprpinfo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import { ADD as ADD_CATEGORYPIC, GET_NEXT_SORT } from '../../providers/mssql/queries/categorypic.js';
|
import {
|
||||||
import { CREATE as CREATE_PIC } from '../../providers/mssql/queries/pic.js';
|
ADD as MSSQL_ADD_CATEGORYPIC,
|
||||||
|
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/mssql/queries/categorypic.js';
|
||||||
|
import {
|
||||||
|
ADD as SQLITE_ADD_CATEGORYPIC,
|
||||||
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/categorypic.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.js';
|
||||||
import processAvifImage from '../../util/processAvifImage.js';
|
import processAvifImage from '../../util/processAvifImage.js';
|
||||||
import newFullHash from '../../util/newFullHash.js';
|
import newFullHash from '../../util/newFullHash.js';
|
||||||
import request from '../request.js';
|
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function addImage(pool) {
|
export default function addImage(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
|
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
|
||||||
|
const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an image to a category, processing it into AVIF full and thumb variants.
|
* Adds an image to a category, processing it into AVIF full and thumb variants.
|
||||||
@@ -14,50 +27,53 @@ export default function addImage(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} categoryId - The id of the category.
|
* @param {number} categoryId - The id of the category.
|
||||||
* @param {Buffer} image - The source image buffer.
|
* @param {Buffer} image - The source image buffer.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info.
|
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const result = await store.category.addImage(42, 5, imageBuffer);
|
* const result = await store.category.addImage(42, 5, imageBuffer);
|
||||||
* // => { id, fullHash, thumbW, thumbH, thumbHash }
|
* // => { id, fullHash, thumbW, thumbH, thumbHash }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, categoryId, image) => {
|
return async (googleAuthId, categoryId, image, tx) => {
|
||||||
await access(googleAuthId, categoryId);
|
const runAdd = async (runner) => {
|
||||||
|
await access(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
const processed = await processAvifImage(image);
|
const processed = await processAvifImage(image);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const sortResult = await runner.query(getSortSql, { categoryId });
|
||||||
await tx.begin();
|
const sort = sortResult.rows[0].sort;
|
||||||
|
|
||||||
try {
|
|
||||||
const sortResult = await request(pool, tx)
|
|
||||||
.input('categoryId', categoryId)
|
|
||||||
.query(GET_NEXT_SORT);
|
|
||||||
|
|
||||||
const sort = sortResult.recordset[0].sort;
|
|
||||||
const fullHash = newFullHash();
|
const fullHash = newFullHash();
|
||||||
|
|
||||||
const picResult = await request(pool, tx)
|
if (isSqlite) {
|
||||||
.input('w', processed.full.w)
|
if (runner.writePicFull) {
|
||||||
.input('h', processed.full.h)
|
await runner.writePicFull(fullHash, processed.full.buffer);
|
||||||
.input('size', processed.full.size)
|
await runner.writePicThumb(fullHash, processed.thumb.buffer);
|
||||||
.input('hash', fullHash)
|
} else if (provider?.writePicFull) {
|
||||||
.input('thumbw', processed.thumb.w)
|
await provider.writePicFull(fullHash, processed.full.buffer);
|
||||||
.input('thumbh', processed.thumb.h)
|
await provider.writePicThumb(fullHash, processed.thumb.buffer);
|
||||||
.input('thumbhash', processed.thumb.hash)
|
}
|
||||||
.input('source', '')
|
}
|
||||||
.input('fullbinary', processed.full.buffer)
|
|
||||||
.input('thumbbinary', processed.thumb.buffer)
|
|
||||||
.query(CREATE_PIC);
|
|
||||||
|
|
||||||
const picId = picResult.recordset[0].id;
|
const picResult = await runner.query(createPicSql, {
|
||||||
|
w: processed.full.w,
|
||||||
|
h: processed.full.h,
|
||||||
|
size: processed.full.size,
|
||||||
|
hash: fullHash,
|
||||||
|
thumbw: processed.thumb.w,
|
||||||
|
thumbh: processed.thumb.h,
|
||||||
|
thumbhash: processed.thumb.hash,
|
||||||
|
source: '',
|
||||||
|
fullbinary: processed.full.buffer,
|
||||||
|
thumbbinary: processed.thumb.buffer,
|
||||||
|
});
|
||||||
|
|
||||||
await request(pool, tx)
|
const picId = picResult.rows[0]?.id ?? picResult.insertId;
|
||||||
.input('categoryId', categoryId)
|
|
||||||
.input('picId', picId)
|
|
||||||
.input('sort', sort)
|
|
||||||
.query(ADD_CATEGORYPIC);
|
|
||||||
|
|
||||||
await tx.commit();
|
await runner.query(addCategoryPicSql, {
|
||||||
|
categoryId,
|
||||||
|
picId,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: categoryId,
|
id: categoryId,
|
||||||
@@ -66,13 +82,14 @@ export default function addImage(pool) {
|
|||||||
thumbH: processed.thumb.h,
|
thumbH: processed.thumb.h,
|
||||||
thumbHash: processed.thumb.hash,
|
thumbHash: processed.thumb.hash,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runAdd(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runAdd);
|
||||||
|
}
|
||||||
|
return runAdd(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
CREATE,
|
CREATE as MSSQL_CREATE,
|
||||||
GET_NEXT_SIBLING_SORT,
|
GET_NEXT_SIBLING_SORT as MSSQL_GET_NEXT_SIBLING_SORT,
|
||||||
} from '../../providers/mssql/queries/category.js';
|
} from '../../providers/mssql/queries/category.js';
|
||||||
import { CREATE as CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js';
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_NEXT_SIBLING_SORT as SQLITE_GET_NEXT_SIBLING_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/category.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_CATEGORYTEXT } from '../../providers/sqlite3/queries/categorytext.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
import request from '../request.js';
|
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function createUnderParent(pool) {
|
export default function createUnderParent(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const getSortSql = isSqlite
|
||||||
|
? SQLITE_GET_NEXT_SIBLING_SORT
|
||||||
|
: MSSQL_GET_NEXT_SIBLING_SORT;
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
const createTextSql = isSqlite
|
||||||
|
? SQLITE_CREATE_CATEGORYTEXT
|
||||||
|
: MSSQL_CREATE_CATEGORYTEXT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new category under a given parent category.
|
* Creates a new category under a given parent category.
|
||||||
@@ -17,56 +31,50 @@ export default function createUnderParent(pool) {
|
|||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.parentId - The id of the parent category.
|
* @param {number} input.parentId - The id of the parent category.
|
||||||
* @param {string} input.name - The category name.
|
* @param {string} input.name - The category name.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>} The created category.
|
* @returns {Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>} The created category.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const category = await store.category.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Drinks' });
|
* const category = await store.category.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Drinks' });
|
||||||
* // => { id, parentId, sort, name, slug }
|
* // => { id, parentId, sort, name, slug }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, parentId, name }) => {
|
return async ({ googleAuthId, parentId, name }, tx) => {
|
||||||
const { languageId } = await access(googleAuthId, parentId);
|
const runCreate = async (runner) => {
|
||||||
|
const { languageId } = await access(googleAuthId, parentId, runner);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const sortResult = await runner.query(getSortSql, { parentId });
|
||||||
await tx.begin();
|
const sort = sortResult.rows[0].sort;
|
||||||
|
|
||||||
try {
|
|
||||||
const sortResult = await request(pool, tx)
|
|
||||||
.input('parentId', parentId)
|
|
||||||
.query(GET_NEXT_SIBLING_SORT);
|
|
||||||
|
|
||||||
const sort = sortResult.recordset[0].sort;
|
|
||||||
const slug = slugify(name);
|
const slug = slugify(name);
|
||||||
|
|
||||||
const category = await request(pool, tx)
|
const category = await runner.query(createSql, {
|
||||||
.input('parentId', parentId)
|
parentId,
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(CREATE);
|
});
|
||||||
|
|
||||||
const { id, parentId: resolvedParentId } = category.recordset[0];
|
const id = category.rows[0]?.id ?? category.insertId;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(createTextSql, {
|
||||||
.input('categoryId', id)
|
categoryId: id,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('name', name)
|
name,
|
||||||
.input('slug', slug)
|
slug,
|
||||||
.query(CREATE_CATEGORYTEXT);
|
});
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
parentId: resolvedParentId,
|
parentId,
|
||||||
sort,
|
sort,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runCreate(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { DELETE } from '../../providers/mssql/queries/category.js';
|
import { DELETE as MSSQL_DELETE } from '../../providers/mssql/queries/category.js';
|
||||||
|
import { DELETE as SQLITE_DELETE } from '../../providers/sqlite3/queries/category.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a category, refusing to delete the company's root category.
|
* Deletes a category, refusing to delete the company's root category.
|
||||||
@@ -10,19 +13,20 @@ export default function del(pool) {
|
|||||||
* @param {object} input - The delete input.
|
* @param {object} input - The delete input.
|
||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.id - The id of the category to delete.
|
* @param {number} input.id - The id of the category to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the category is the root category.
|
* @throws {Error} If the category is the root category.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.category.delete({ googleAuthId: 42, id: 5 });
|
* await store.category.delete({ googleAuthId: 42, id: 5 });
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, id }) => {
|
return async ({ googleAuthId, id }, tx) => {
|
||||||
const { rootCategoryId } = await access(googleAuthId, id);
|
const { rootCategoryId } = await access(googleAuthId, id, tx);
|
||||||
|
|
||||||
if (id === rootCategoryId) {
|
if (id === rootCategoryId) {
|
||||||
throw new Error('Cannot delete root category');
|
throw new Error('Cannot delete root category');
|
||||||
}
|
}
|
||||||
|
|
||||||
await pool.request().input('id', id).query(DELETE);
|
await makeRunner(provider, tx).query(sql, { id });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { DELETE_BY_CATEGORY } from '../../providers/mssql/queries/categorypic.js';
|
import { DELETE_BY_CATEGORY as MSSQL_DELETE } from '../../providers/mssql/queries/categorypic.js';
|
||||||
|
import { DELETE_BY_CATEGORY as SQLITE_DELETE } from '../../providers/sqlite3/queries/categorypic.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function deleteImage(pool) {
|
export default function deleteImage(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the image of a category.
|
* Deletes the image of a category.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} categoryId - The id of the category.
|
* @param {number} categoryId - The id of the category.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>} The cleared image info.
|
* @returns {Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>} The cleared image info.
|
||||||
* @throws {Error} If the image is not found.
|
* @throws {Error} If the image is not found.
|
||||||
*
|
*
|
||||||
@@ -16,15 +20,13 @@ export default function deleteImage(pool) {
|
|||||||
* const result = await store.category.deleteImage(42, 5);
|
* const result = await store.category.deleteImage(42, 5);
|
||||||
* // => { id: 5, thumbW: null, thumbH: null, thumbHash: null }
|
* // => { id: 5, thumbW: null, thumbH: null, thumbHash: null }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, categoryId) => {
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
await access(googleAuthId, categoryId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
await access(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, { categoryId });
|
||||||
.request()
|
|
||||||
.input('categoryId', categoryId)
|
|
||||||
.query(DELETE_BY_CATEGORY);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Image not found');
|
throw new Error('Image not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
CREATE,
|
CREATE as MSSQL_CREATE,
|
||||||
GET_BY_PARENT_AND_NAME,
|
GET_BY_PARENT_AND_NAME as MSSQL_GET_BY_PARENT_AND_NAME,
|
||||||
GET_NEXT_SIBLING_SORT,
|
GET_NEXT_SIBLING_SORT as MSSQL_GET_NEXT_SIBLING_SORT,
|
||||||
} from '../../providers/mssql/queries/category.js';
|
} from '../../providers/mssql/queries/category.js';
|
||||||
import { CREATE as CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js';
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_BY_PARENT_AND_NAME as SQLITE_GET_BY_PARENT_AND_NAME,
|
||||||
|
GET_NEXT_SIBLING_SORT as SQLITE_GET_NEXT_SIBLING_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/category.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_CATEGORYTEXT } from '../../providers/mssql/queries/categorytext.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_CATEGORYTEXT } from '../../providers/sqlite3/queries/categorytext.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
import request from '../request.js';
|
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
// Splits a "A | B | C" breadcrumb string into trimmed, non-empty name parts,
|
// Splits a "A | B | C" breadcrumb string into trimmed, non-empty name parts,
|
||||||
// ordered root-first.
|
// ordered root-first.
|
||||||
@@ -17,8 +23,20 @@ export function splitPath(path) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ensureByPath(pool) {
|
export default function ensureByPath(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const getSortSql = isSqlite
|
||||||
|
? SQLITE_GET_NEXT_SIBLING_SORT
|
||||||
|
: MSSQL_GET_NEXT_SIBLING_SORT;
|
||||||
|
const createCategorySql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
const createTextSql = isSqlite
|
||||||
|
? SQLITE_CREATE_CATEGORYTEXT
|
||||||
|
: MSSQL_CREATE_CATEGORYTEXT;
|
||||||
|
const getByNameSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_PARENT_AND_NAME
|
||||||
|
: MSSQL_GET_BY_PARENT_AND_NAME;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a category by breadcrumb path under a given parent, creating any
|
* Resolves a category by breadcrumb path under a given parent, creating any
|
||||||
@@ -43,62 +61,54 @@ export default function ensureByPath(pool) {
|
|||||||
|
|
||||||
let currentId = parentId;
|
let currentId = parentId;
|
||||||
|
|
||||||
const createCategory = async (tx, parentId, name, languageId) => {
|
const createCategory = async (runner, parentId, name, languageId) => {
|
||||||
const sortResult = await request(pool, tx)
|
const sortResult = await runner.query(getSortSql, { parentId });
|
||||||
.input('parentId', parentId)
|
const sort = sortResult.rows[0].sort;
|
||||||
.query(GET_NEXT_SIBLING_SORT);
|
|
||||||
const sort = sortResult.recordset[0].sort;
|
|
||||||
const slug = slugify(name);
|
const slug = slugify(name);
|
||||||
|
|
||||||
const category = await request(pool, tx)
|
const category = await runner.query(createCategorySql, {
|
||||||
.input('parentId', parentId)
|
parentId,
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(CREATE);
|
});
|
||||||
const id = category.recordset[0].id;
|
const id = category.rows[0]?.id ?? category.insertId;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(createTextSql, {
|
||||||
.input('categoryId', id)
|
categoryId: id,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('name', name)
|
name,
|
||||||
.input('slug', slug)
|
slug,
|
||||||
.query(CREATE_CATEGORYTEXT);
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
const { languageId } = await access(googleAuthId, currentId);
|
const { languageId } = await access(googleAuthId, currentId, tx);
|
||||||
|
|
||||||
const existing = await request(pool, tx)
|
const existing = await makeRunner(provider, tx).query(getByNameSql, {
|
||||||
.input('parentId', currentId)
|
parentId: currentId,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('name', name)
|
name,
|
||||||
.query(GET_BY_PARENT_AND_NAME);
|
});
|
||||||
|
|
||||||
if (existing.recordset[0]) {
|
if (existing.rows[0]) {
|
||||||
currentId = existing.recordset[0].id;
|
currentId = existing.rows[0].id;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
// Run within the caller's transaction (e.g. article import row).
|
// Run within the caller's transaction (e.g. article import row).
|
||||||
currentId = await createCategory(tx, currentId, name, languageId);
|
currentId = await createCategory(makeRunner(provider, tx), currentId, name, languageId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standalone use: own transaction per created category.
|
// Standalone use: own transaction per created category.
|
||||||
const ownTx = pool.transaction();
|
if (typeof provider?.transaction === 'function') {
|
||||||
await ownTx.begin();
|
currentId = await provider.transaction((runner) =>
|
||||||
try {
|
createCategory(runner, currentId, name, languageId),
|
||||||
currentId = await createCategory(ownTx, currentId, name, languageId);
|
);
|
||||||
await ownTx.commit();
|
} else {
|
||||||
} catch (err) {
|
currentId = await createCategory(makeRunner(provider), currentId, name, languageId);
|
||||||
try {
|
|
||||||
await ownTx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
import queryCategoryArticles from './queryCategoryArticles.js';
|
import queryCategoryArticles from './queryCategoryArticles.js';
|
||||||
|
|
||||||
export default function getCategoryArticles(pool) {
|
export default function getCategoryArticles(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
const articles = queryCategoryArticles(pool);
|
const articles = queryCategoryArticles(provider);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the articles belonging to a single category.
|
* Returns the articles belonging to a single category.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} categoryId - The id of the category.
|
* @param {number} categoryId - The id of the category.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<object>>} The articles in the category.
|
* @returns {Promise<Array<object>>} The articles in the category.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const articles = await store.category.getCategoryArticles(42, 5);
|
* const articles = await store.category.getCategoryArticles(42, 5);
|
||||||
* // => [{ id, sku, name, ... }]
|
* // => [{ id, sku, name, ... }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, categoryId) => {
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
const { languageId, posCustomerGroupId } = await access(
|
const { languageId, posCustomerGroupId } = await access(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
categoryId,
|
categoryId,
|
||||||
|
tx,
|
||||||
);
|
);
|
||||||
|
|
||||||
return articles({ categoryId, languageId, posCustomerGroupId });
|
return articles({ categoryId, languageId, posCustomerGroupId }, tx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,44 @@
|
|||||||
import { GET_TREE_FOR_COMPANY } from '../../providers/mssql/queries/category.js';
|
import { GET_TREE_FOR_COMPANY as MSSQL_GET_TREE } from '../../providers/mssql/queries/category.js';
|
||||||
|
import { GET_TREE_FOR_COMPANY as SQLITE_GET_TREE } from '../../providers/sqlite3/queries/category.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import queryCategoryArticles from './queryCategoryArticles.js';
|
import queryCategoryArticles from './queryCategoryArticles.js';
|
||||||
import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js';
|
import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function getTreeForCompany(pool) {
|
export default function getTreeForCompany(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const articles = queryCategoryArticles(pool);
|
const articles = queryCategoryArticles(provider);
|
||||||
const posterminalIds = getCategoryPosterminalIds(pool);
|
const posterminalIds = getCategoryPosterminalIds(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3' ? SQLITE_GET_TREE : MSSQL_GET_TREE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the full category tree for the user's selected company together
|
* Returns the full category tree for the user's selected company together
|
||||||
* with its articles and the posterminal ids assigned to each category.
|
* with its articles and the posterminal ids assigned to each category.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>} The category tree, articles and language.
|
* @returns {Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>} The category tree, articles and language.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const tree = await store.category.getTreeForCompany(42);
|
* const tree = await store.category.getTreeForCompany(42);
|
||||||
* // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
* // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const { companyId, rootCategoryId, languageId, posCustomerGroupId } =
|
const { companyId, rootCategoryId, languageId, posCustomerGroupId } =
|
||||||
await access(googleAuthId);
|
await access(googleAuthId, tx);
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const [treeResult, articleRows, ptMap] = await Promise.all([
|
const [treeResult, articleRows, ptMap] = await Promise.all([
|
||||||
pool
|
runner.query(sql, {
|
||||||
.request()
|
rootId: rootCategoryId,
|
||||||
.input('rootId', rootCategoryId)
|
languageId,
|
||||||
.input('languageId', languageId)
|
}),
|
||||||
.query(GET_TREE_FOR_COMPANY),
|
articles({ categoryId: rootCategoryId, languageId, posCustomerGroupId }, tx),
|
||||||
articles({ categoryId: rootCategoryId, languageId, posCustomerGroupId }),
|
posterminalIds(companyId, tx),
|
||||||
posterminalIds(companyId),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tree = treeResult.recordset.map((row) => ({
|
const tree = treeResult.rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
posterminalIds: ptMap[row.id] ?? [],
|
posterminalIds: ptMap[row.id] ?? [],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import {
|
import {
|
||||||
GET_NEXT_SIBLING_SORT,
|
GET_NEXT_SIBLING_SORT as MSSQL_GET_NEXT_SIBLING_SORT,
|
||||||
IS_ANCESTOR,
|
IS_ANCESTOR as MSSQL_IS_ANCESTOR,
|
||||||
MOVE,
|
MOVE as MSSQL_MOVE,
|
||||||
} from '../../providers/mssql/queries/category.js';
|
} from '../../providers/mssql/queries/category.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
GET_NEXT_SIBLING_SORT as SQLITE_GET_NEXT_SIBLING_SORT,
|
||||||
|
IS_ANCESTOR as SQLITE_IS_ANCESTOR,
|
||||||
|
MOVE as SQLITE_MOVE,
|
||||||
|
} from '../../providers/sqlite3/queries/category.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function move(pool) {
|
export default function move(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const isAncestorSql = isSqlite ? SQLITE_IS_ANCESTOR : MSSQL_IS_ANCESTOR;
|
||||||
|
const getSortSql = isSqlite
|
||||||
|
? SQLITE_GET_NEXT_SIBLING_SORT
|
||||||
|
: MSSQL_GET_NEXT_SIBLING_SORT;
|
||||||
|
const moveSql = isSqlite ? SQLITE_MOVE : MSSQL_MOVE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves a category to a new parent, preventing cycles and cross-company moves.
|
* Moves a category to a new parent, preventing cycles and cross-company moves.
|
||||||
@@ -16,6 +28,7 @@ export default function move(pool) {
|
|||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.id - The id of the category to move.
|
* @param {number} input.id - The id of the category to move.
|
||||||
* @param {number} input.parentId - The id of the new parent category.
|
* @param {number} input.parentId - The id of the new parent category.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, parentId: number, sort: number}>} The moved category.
|
* @returns {Promise<{id: number, parentId: number, sort: number}>} The moved category.
|
||||||
* @throws {Error} If the move would create a cycle or cross companies.
|
* @throws {Error} If the move would create a cycle or cross companies.
|
||||||
*
|
*
|
||||||
@@ -23,59 +36,49 @@ export default function move(pool) {
|
|||||||
* const result = await store.category.move({ googleAuthId: 42, id: 5, parentId: 9 });
|
* const result = await store.category.move({ googleAuthId: 42, id: 5, parentId: 9 });
|
||||||
* // => { id, parentId, sort }
|
* // => { id, parentId, sort }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, id, parentId }) => {
|
return async ({ googleAuthId, id, parentId }, tx) => {
|
||||||
// Source category auth (throws if missing / not authorized).
|
const runMove = async (runner) => {
|
||||||
const source = await access(googleAuthId, id);
|
// Source category auth (throws if missing / not authorized).
|
||||||
// Target must be a valid category for the same authorized user/company.
|
const source = await access(googleAuthId, id, runner);
|
||||||
const target = await access(googleAuthId, parentId);
|
// Target must be a valid category for the same authorized user/company.
|
||||||
|
const target = await access(googleAuthId, parentId, runner);
|
||||||
|
|
||||||
if (target.companyId !== source.companyId) {
|
if (target.companyId !== source.companyId) {
|
||||||
throw new Error('Parent not found');
|
throw new Error('Parent not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === parentId) {
|
if (id === parentId) {
|
||||||
throw new Error('Invalid parent');
|
throw new Error('Invalid parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cycle = await pool
|
const cycle = await runner.query(isAncestorSql, { id, parentId });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.input('parentId', parentId)
|
|
||||||
.query(IS_ANCESTOR);
|
|
||||||
|
|
||||||
if (cycle.recordset[0]) {
|
if (cycle.rows[0]) {
|
||||||
throw new Error('Invalid parent');
|
throw new Error('Invalid parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const sortResult = await runner.query(getSortSql, { parentId });
|
||||||
await tx.begin();
|
const sort = sortResult.rows[0].sort;
|
||||||
|
|
||||||
try {
|
const result = await runner.query(moveSql, {
|
||||||
const sortResult = await request(pool, tx)
|
id,
|
||||||
.input('parentId', parentId)
|
parentId,
|
||||||
.query(GET_NEXT_SIBLING_SORT);
|
sort,
|
||||||
const sort = sortResult.recordset[0].sort;
|
});
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
if (!result.rowsAffected) {
|
||||||
.input('id', id)
|
|
||||||
.input('parentId', parentId)
|
|
||||||
.input('sort', sort)
|
|
||||||
.query(MOVE);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
|
||||||
throw new Error('Category not found');
|
throw new Error('Category not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return { id, parentId, sort };
|
return { id, parentId, sort };
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runMove(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runMove);
|
||||||
|
}
|
||||||
|
return runMove(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
LIST_CHILD_IDS,
|
LIST_CHILD_IDS as MSSQL_LIST_CHILD_IDS,
|
||||||
UPDATE_SORT,
|
UPDATE_SORT as MSSQL_UPDATE_SORT,
|
||||||
} from '../../providers/mssql/queries/category.js';
|
} from '../../providers/mssql/queries/category.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
LIST_CHILD_IDS as SQLITE_LIST_CHILD_IDS,
|
||||||
|
UPDATE_SORT as SQLITE_UPDATE_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/category.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function order(pool) {
|
export default function order(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const listChildIdsSql = isSqlite
|
||||||
|
? SQLITE_LIST_CHILD_IDS
|
||||||
|
: MSSQL_LIST_CHILD_IDS;
|
||||||
|
const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the child categories of a given parent.
|
* Reorders the child categories of a given parent.
|
||||||
@@ -15,6 +24,7 @@ export default function order(pool) {
|
|||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.parentId - The id of the parent category.
|
* @param {number} input.parentId - The id of the parent category.
|
||||||
* @param {Array<number>} input.categoryIds - The child category ids in their new order.
|
* @param {Array<number>} input.categoryIds - The child category ids in their new order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{categoryIds: Array<number>}>} The applied order.
|
* @returns {Promise<{categoryIds: Array<number>}>} The applied order.
|
||||||
* @throws {Error} If the order is invalid.
|
* @throws {Error} If the order is invalid.
|
||||||
*
|
*
|
||||||
@@ -22,55 +32,48 @@ export default function order(pool) {
|
|||||||
* const result = await store.category.order({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] });
|
* const result = await store.category.order({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] });
|
||||||
* // => { categoryIds: [3, 1, 2] }
|
* // => { categoryIds: [3, 1, 2] }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, parentId, categoryIds }) => {
|
return async ({ googleAuthId, parentId, categoryIds }, tx) => {
|
||||||
if (!Array.isArray(categoryIds)) {
|
if (!Array.isArray(categoryIds)) {
|
||||||
throw new Error('Invalid category order');
|
throw new Error('Invalid category order');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth + ensures parentId is a real category of the user's company.
|
const runOrder = async (runner) => {
|
||||||
await access(googleAuthId, parentId);
|
// Auth + ensures parentId is a real category of the user's company.
|
||||||
|
await access(googleAuthId, parentId, runner);
|
||||||
|
|
||||||
const children = await pool
|
const children = await runner.query(listChildIdsSql, { parentId });
|
||||||
.request()
|
|
||||||
.input('parentId', parentId)
|
|
||||||
.query(LIST_CHILD_IDS);
|
|
||||||
|
|
||||||
const existing = new Set(children.recordset.map((r) => r.id));
|
const existing = new Set(children.rows.map((r) => r.id));
|
||||||
|
|
||||||
if (categoryIds.length !== existing.size) {
|
if (categoryIds.length !== existing.size) {
|
||||||
throw new Error('Invalid category order');
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
for (const categoryId of categoryIds) {
|
|
||||||
if (!Number.isFinite(categoryId) || !existing.has(categoryId) || seen.has(categoryId)) {
|
|
||||||
throw new Error('Invalid category order');
|
throw new Error('Invalid category order');
|
||||||
}
|
}
|
||||||
seen.add(categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const seen = new Set();
|
||||||
await tx.begin();
|
for (const categoryId of categoryIds) {
|
||||||
|
if (!Number.isFinite(categoryId) || !existing.has(categoryId) || seen.has(categoryId)) {
|
||||||
try {
|
throw new Error('Invalid category order');
|
||||||
for (let sort = 0; sort < categoryIds.length; sort++) {
|
}
|
||||||
await request(pool, tx)
|
seen.add(categoryId);
|
||||||
.input('id', categoryIds[sort])
|
|
||||||
.input('parentId', parentId)
|
|
||||||
.input('sort', sort)
|
|
||||||
.query(UPDATE_SORT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
for (let sort = 0; sort < categoryIds.length; sort++) {
|
||||||
|
await runner.query(updateSortSql, {
|
||||||
|
id: categoryIds[sort],
|
||||||
|
parentId,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { categoryIds };
|
return { categoryIds };
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runOrder(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runOrder);
|
||||||
|
}
|
||||||
|
return runOrder(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { GET_CATEGORY_ARTICLES } from '../../providers/mssql/queries/category.js';
|
import { GET_CATEGORY_ARTICLES as MSSQL_GET } from '../../providers/mssql/queries/category.js';
|
||||||
|
import { GET_CATEGORY_ARTICLES as SQLITE_GET } from '../../providers/sqlite3/queries/category.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function queryCategoryArticles(pool) {
|
export default function queryCategoryArticles(provider) {
|
||||||
return async ({ categoryId, languageId, posCustomerGroupId }) => {
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET;
|
||||||
const result = await pool
|
|
||||||
.request()
|
|
||||||
.input('categoryId', categoryId)
|
|
||||||
.input('languageId', languageId)
|
|
||||||
.input('posCustomerGroupId', posCustomerGroupId)
|
|
||||||
.query(GET_CATEGORY_ARTICLES);
|
|
||||||
|
|
||||||
return result.recordset;
|
return async ({ categoryId, languageId, posCustomerGroupId }, tx) => {
|
||||||
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
|
categoryId,
|
||||||
|
languageId,
|
||||||
|
posCustomerGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { UPDATE_BY_CATEGORY_AND_LANGUAGE } from '../../providers/mssql/queries/categorytext.js';
|
import { UPDATE_BY_CATEGORY_AND_LANGUAGE as MSSQL_UPDATE } from '../../providers/mssql/queries/categorytext.js';
|
||||||
|
import { UPDATE_BY_CATEGORY_AND_LANGUAGE as SQLITE_UPDATE } from '../../providers/sqlite3/queries/categorytext.js';
|
||||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||||
import slugify from '../slugify.js';
|
import slugify from '../slugify.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function rename(pool) {
|
export default function rename(provider) {
|
||||||
const access = resolveCategoryAccess(pool);
|
const access = resolveCategoryAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renames a category and updates its slug.
|
* Renames a category and updates its slug.
|
||||||
@@ -12,6 +15,7 @@ export default function rename(pool) {
|
|||||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} input.id - The id of the category to rename.
|
* @param {number} input.id - The id of the category to rename.
|
||||||
* @param {string} input.name - The new category name.
|
* @param {string} input.name - The new category name.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, name: string, slug: string}>} The renamed category.
|
* @returns {Promise<{id: number, name: string, slug: string}>} The renamed category.
|
||||||
* @throws {Error} If the name is empty or the category is not found.
|
* @throws {Error} If the name is empty or the category is not found.
|
||||||
*
|
*
|
||||||
@@ -19,25 +23,24 @@ export default function rename(pool) {
|
|||||||
* const result = await store.category.rename({ googleAuthId: 42, id: 5, name: 'Soft Drinks' });
|
* const result = await store.category.rename({ googleAuthId: 42, id: 5, name: 'Soft Drinks' });
|
||||||
* // => { id, name, slug }
|
* // => { id, name, slug }
|
||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, id, name }) => {
|
return async ({ googleAuthId, id, name }, tx) => {
|
||||||
if (!name?.trim()) {
|
if (!name?.trim()) {
|
||||||
throw new Error('Name required');
|
throw new Error('Name required');
|
||||||
}
|
}
|
||||||
const cleaned = name.trim();
|
const cleaned = name.trim();
|
||||||
|
|
||||||
const { languageId } = await access(googleAuthId, id);
|
const { languageId } = await access(googleAuthId, id, tx);
|
||||||
|
|
||||||
const slug = slugify(cleaned);
|
const slug = slugify(cleaned);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
categoryId: id,
|
||||||
.input('categoryId', id)
|
languageId,
|
||||||
.input('languageId', languageId)
|
name: cleaned,
|
||||||
.input('name', cleaned)
|
slug,
|
||||||
.input('slug', slug)
|
});
|
||||||
.query(UPDATE_BY_CATEGORY_AND_LANGUAGE);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Category not found');
|
throw new Error('Category not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import { RESOLVE_CATEGORY_ACCESS } from '../../providers/mssql/queries/category.js';
|
import { RESOLVE_CATEGORY_ACCESS as MSSQL_RESOLVE } from '../../providers/mssql/queries/category.js';
|
||||||
|
import { RESOLVE_CATEGORY_ACCESS as SQLITE_RESOLVE } from '../../providers/sqlite3/queries/category.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function resolveCategoryAccess(pool) {
|
export default function resolveCategoryAccess(provider) {
|
||||||
return async (googleAuthId, categoryId) => {
|
const sql =
|
||||||
const result = await pool
|
provider?.dialect === 'sqlite3' ? SQLITE_RESOLVE : MSSQL_RESOLVE;
|
||||||
.request()
|
|
||||||
.input('categoryId', categoryId)
|
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(RESOLVE_CATEGORY_ACCESS);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
|
categoryId,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Parent not found');
|
throw new Error('Parent not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import { POS_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
import { POS_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
||||||
import { CREATE } from '../../providers/mssql/queries/company.js';
|
import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/company.js';
|
||||||
import { CREATE as CREATE_CATEGORY } from '../../providers/mssql/queries/category.js';
|
import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/company.js';
|
||||||
|
import { CREATE as MSSQL_CREATE_CATEGORY } from '../../providers/mssql/queries/category.js';
|
||||||
|
import { CREATE as SQLITE_CREATE_CATEGORY } from '../../providers/sqlite3/queries/category.js';
|
||||||
import {
|
import {
|
||||||
CREATE_WITHOUT_COMPANY as CREATE_CUSTOMERGROUP,
|
CREATE_WITHOUT_COMPANY as MSSQL_CREATE_CUSTOMERGROUP,
|
||||||
SET_COMPANY as SET_CUSTOMERGROUP_COMPANY,
|
SET_COMPANY as MSSQL_SET_CUSTOMERGROUP_COMPANY,
|
||||||
} from '../../providers/mssql/queries/customergroup.js';
|
} from '../../providers/mssql/queries/customergroup.js';
|
||||||
import { ADD as ADD_COMPANYUSER } from '../../providers/mssql/queries/companyuser.js';
|
import {
|
||||||
|
CREATE_WITHOUT_COMPANY as SQLITE_CREATE_CUSTOMERGROUP,
|
||||||
|
SET_COMPANY as SQLITE_SET_CUSTOMERGROUP_COMPANY,
|
||||||
|
} from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
|
import { ADD as MSSQL_ADD_COMPANYUSER } from '../../providers/mssql/queries/companyuser.js';
|
||||||
|
import { ADD as SQLITE_ADD_COMPANYUSER } from '../../providers/sqlite3/queries/companyuser.js';
|
||||||
import getByName from './getByName.js';
|
import getByName from './getByName.js';
|
||||||
import langEnsure from '../lang/ensure.js';
|
import langEnsure from '../lang/ensure.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
import request from '../request.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const nameTaken = getByName(pool);
|
const nameTaken = getByName(provider);
|
||||||
const ensureLang = langEnsure(pool);
|
const ensureLang = langEnsure(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
|
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const createCategorySql = isSqlite ? SQLITE_CREATE_CATEGORY : MSSQL_CREATE_CATEGORY;
|
||||||
|
const createCustGroupSql = isSqlite ? SQLITE_CREATE_CUSTOMERGROUP : MSSQL_CREATE_CUSTOMERGROUP;
|
||||||
|
const createCompanySql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
const setCustGroupCompanySql = isSqlite ? SQLITE_SET_CUSTOMERGROUP_COMPANY : MSSQL_SET_CUSTOMERGROUP_COMPANY;
|
||||||
|
const addCompanyUserSql = isSqlite ? SQLITE_ADD_COMPANYUSER : MSSQL_ADD_COMPANYUSER;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new company together with its root category, POS customer group,
|
* Creates a new company together with its root category, POS customer group,
|
||||||
@@ -26,6 +40,7 @@ export default function create(pool) {
|
|||||||
* @param {number} input.taxrate - The standard tax rate in percent.
|
* @param {number} input.taxrate - The standard tax rate in percent.
|
||||||
* @param {number} input.reducedtaxrate - The reduced tax rate in percent.
|
* @param {number} input.reducedtaxrate - The reduced tax rate in percent.
|
||||||
* @param {number} input.googleAuthId - The id of the Google auth user to link.
|
* @param {number} input.googleAuthId - The id of the Google auth user to link.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>} The created company ids.
|
* @returns {Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>} The created company ids.
|
||||||
* @throws {Error} If the company name is already taken.
|
* @throws {Error} If the company name is already taken.
|
||||||
*
|
*
|
||||||
@@ -39,60 +54,52 @@ export default function create(pool) {
|
|||||||
* });
|
* });
|
||||||
* // => { id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId, posCustomerGroupId }
|
* // => { id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId, posCustomerGroupId }
|
||||||
*/
|
*/
|
||||||
return async ({ iso639, name, taxrate, reducedtaxrate, googleAuthId }) => {
|
return async ({ iso639, name, taxrate, reducedtaxrate, googleAuthId }, tx) => {
|
||||||
const tx = pool.transaction();
|
const runCreate = async (runner) => {
|
||||||
await tx.begin();
|
const existing = await nameTaken(name, runner);
|
||||||
|
|
||||||
try {
|
|
||||||
const existing = await nameTaken(name, tx);
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new Error('Company name not available');
|
throw new Error('Company name not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageId = await ensureLang(iso639, tx);
|
const languageId = await ensureLang(iso639, runner);
|
||||||
const taxrateId = await ensureTax(taxrate, tx);
|
const taxrateId = await ensureTax(taxrate, runner);
|
||||||
const reducedtaxrateId = await ensureTax(reducedtaxrate, tx);
|
const reducedtaxrateId = await ensureTax(reducedtaxrate, runner);
|
||||||
const taxfreerateId = await ensureTax(0, tx);
|
const taxfreerateId = await ensureTax(0, runner);
|
||||||
|
|
||||||
const category = await request(pool, tx)
|
const category = await runner.query(createCategorySql, {
|
||||||
.input('parentId', null)
|
parentId: null,
|
||||||
.input('sort', 0)
|
sort: 0,
|
||||||
.query(CREATE_CATEGORY);
|
});
|
||||||
|
const rootCategoryId = category.rows[0]?.id ?? category.insertId;
|
||||||
const rootCategoryId = category.recordset[0].id;
|
|
||||||
|
|
||||||
// Circular FK: create POS group first (company_id null), company with
|
// Circular FK: create POS group first (company_id null), company with
|
||||||
// poscustomergroup_id NOT NULL, then link the group back to the company.
|
// poscustomergroup_id NOT NULL, then link the group back to the company.
|
||||||
const posGroup = await request(pool, tx)
|
const posGroup = await runner.query(createCustGroupSql, {
|
||||||
.input('name', POS_GROUP_NAME)
|
name: POS_GROUP_NAME,
|
||||||
.input('sort', 0)
|
sort: 0,
|
||||||
.query(CREATE_CUSTOMERGROUP);
|
});
|
||||||
|
const posCustomerGroupId = posGroup.rows[0]?.id ?? posGroup.insertId;
|
||||||
|
|
||||||
const posCustomerGroupId = posGroup.recordset[0].id;
|
const result = await runner.query(createCompanySql, {
|
||||||
|
name,
|
||||||
|
rootCategoryId,
|
||||||
|
languageId,
|
||||||
|
taxrate: taxrateId,
|
||||||
|
reducedtaxrate: reducedtaxrateId,
|
||||||
|
taxfreerate: taxfreerateId,
|
||||||
|
posCustomerGroupId,
|
||||||
|
});
|
||||||
|
const companyId = result.rows[0]?.id ?? result.insertId;
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
await runner.query(setCustGroupCompanySql, {
|
||||||
.input('name', name)
|
id: posCustomerGroupId,
|
||||||
.input('rootCategoryId', rootCategoryId)
|
companyId,
|
||||||
.input('languageId', languageId)
|
});
|
||||||
.input('taxrate', taxrateId)
|
|
||||||
.input('reducedtaxrate', reducedtaxrateId)
|
|
||||||
.input('taxfreerate', taxfreerateId)
|
|
||||||
.input('posCustomerGroupId', posCustomerGroupId)
|
|
||||||
.query(CREATE);
|
|
||||||
|
|
||||||
const companyId = result.recordset[0].id;
|
await runner.query(addCompanyUserSql, {
|
||||||
|
companyId,
|
||||||
await request(pool, tx)
|
googleAuthId,
|
||||||
.input('id', posCustomerGroupId)
|
});
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(SET_CUSTOMERGROUP_COMPANY);
|
|
||||||
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(ADD_COMPANYUSER);
|
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: companyId,
|
id: companyId,
|
||||||
@@ -103,13 +110,14 @@ export default function create(pool) {
|
|||||||
taxfreerateId,
|
taxfreerateId,
|
||||||
posCustomerGroupId,
|
posCustomerGroupId,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runCreate(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { DELETE } from '../../providers/mssql/queries/company.js';
|
import { DELETE as MSSQL_DELETE } from '../../providers/mssql/queries/company.js';
|
||||||
|
import { DELETE as SQLITE_DELETE } from '../../providers/sqlite3/queries/company.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function del(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
export default function del(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Deletes a company by its id.
|
* Deletes a company by its id.
|
||||||
*
|
*
|
||||||
* @param {number} companyId - The id of the company to delete.
|
* @param {number} companyId - The id of the company to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.company.delete(1);
|
* await store.company.delete(1);
|
||||||
*/
|
*/
|
||||||
return async (companyId) => {
|
return async (companyId, tx) => {
|
||||||
await pool
|
await makeRunner(provider, tx).query(sql, { companyId });
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(DELETE);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { GET_BY_ID } from '../../providers/mssql/queries/company.js';
|
import { GET_BY_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/company.js';
|
||||||
import request from '../request.js';
|
import { GET_BY_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/company.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getById(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
|
||||||
|
|
||||||
export default function getById(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Returns a single company by its id.
|
* Returns a single company by its id.
|
||||||
*
|
*
|
||||||
@@ -14,10 +18,8 @@ export default function getById(pool) {
|
|||||||
* // => { id, name, ... }
|
* // => { id, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (id, tx) => {
|
return async (id, tx) => {
|
||||||
const result = await request(pool, tx)
|
const result = await makeRunner(provider, tx).query(sql, { id });
|
||||||
.input('id', id)
|
|
||||||
.query(GET_BY_ID);
|
|
||||||
|
|
||||||
return result.recordset[0];
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { GET_BY_NAME } from '../../providers/mssql/queries/company.js';
|
import { GET_BY_NAME as MSSQL_GET_BY_NAME } from '../../providers/mssql/queries/company.js';
|
||||||
import request from '../request.js';
|
import { GET_BY_NAME as SQLITE_GET_BY_NAME } from '../../providers/sqlite3/queries/company.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getByName(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_NAME : MSSQL_GET_BY_NAME;
|
||||||
|
|
||||||
export default function getByName(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Returns a single company by its name.
|
* Returns a single company by its name.
|
||||||
*
|
*
|
||||||
@@ -14,10 +18,8 @@ export default function getByName(pool) {
|
|||||||
* // => { id, name, ... }
|
* // => { id, name, ... }
|
||||||
*/
|
*/
|
||||||
return async (name, tx) => {
|
return async (name, tx) => {
|
||||||
const result = await request(pool, tx)
|
const result = await makeRunner(provider, tx).query(sql, { name });
|
||||||
.input('name', name)
|
|
||||||
.query(GET_BY_NAME);
|
|
||||||
|
|
||||||
return result.recordset[0];
|
return result.rows[0];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { LIST } from '../../providers/mssql/queries/company.js';
|
import { LIST as MSSQL_LIST } from '../../providers/mssql/queries/company.js';
|
||||||
|
import { LIST as SQLITE_LIST } from '../../providers/sqlite3/queries/company.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function list(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST;
|
||||||
|
|
||||||
export default function list(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Lists the companies accessible to the given Google auth user.
|
* Lists the companies accessible to the given Google auth user.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<object>>} The accessible company rows.
|
* @returns {Promise<Array<object>>} The accessible company rows.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const companies = await store.company.list(42);
|
* const companies = await store.company.list(42);
|
||||||
* // => [{ id, name, ... }]
|
* // => [{ id, name, ... }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, { googleAuthId });
|
||||||
.request()
|
return result.rows;
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(LIST);
|
|
||||||
return result.recordset;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { UPDATE } from '../../providers/mssql/queries/company.js';
|
import { UPDATE as MSSQL_UPDATE } from '../../providers/mssql/queries/company.js';
|
||||||
|
import { UPDATE as SQLITE_UPDATE } from '../../providers/sqlite3/queries/company.js';
|
||||||
import getById from './getById.js';
|
import getById from './getById.js';
|
||||||
import getByName from './getByName.js';
|
import getByName from './getByName.js';
|
||||||
import langEnsure from '../lang/ensure.js';
|
import langEnsure from '../lang/ensure.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
import request from '../request.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const byId = getById(pool);
|
const byId = getById(provider);
|
||||||
const byName = getByName(pool);
|
const byName = getByName(provider);
|
||||||
const ensureLang = langEnsure(pool);
|
const ensureLang = langEnsure(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an existing company's name, language and tax rates.
|
* Updates an existing company's name, language and tax rates.
|
||||||
@@ -20,6 +23,7 @@ export default function update(pool) {
|
|||||||
* @param {string} input.name - The new company name (must be unique).
|
* @param {string} input.name - The new company name (must be unique).
|
||||||
* @param {number} input.taxrate - The standard tax rate in percent.
|
* @param {number} input.taxrate - The standard tax rate in percent.
|
||||||
* @param {number} input.reducedtaxrate - The reduced tax rate in percent.
|
* @param {number} input.reducedtaxrate - The reduced tax rate in percent.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>} The updated company ids.
|
* @returns {Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>} The updated company ids.
|
||||||
* @throws {Error} If the company is not found or the name is taken.
|
* @throws {Error} If the company is not found or the name is taken.
|
||||||
*
|
*
|
||||||
@@ -27,36 +31,31 @@ export default function update(pool) {
|
|||||||
* const result = await store.company.update(1, { iso639: 'de', name: 'Acme', taxrate: 19, reducedtaxrate: 7 });
|
* const result = await store.company.update(1, { iso639: 'de', name: 'Acme', taxrate: 19, reducedtaxrate: 7 });
|
||||||
* // => { id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }
|
* // => { id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId }
|
||||||
*/
|
*/
|
||||||
return async (id, { iso639, name, taxrate, reducedtaxrate }) => {
|
return async (id, { iso639, name, taxrate, reducedtaxrate }, tx) => {
|
||||||
const tx = pool.transaction();
|
const runUpdate = async (runner) => {
|
||||||
await tx.begin();
|
const company = await byId(id, runner);
|
||||||
|
|
||||||
try {
|
|
||||||
const company = await byId(id, tx);
|
|
||||||
if (!company) {
|
if (!company) {
|
||||||
throw new Error('Company not found');
|
throw new Error('Company not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name !== company.name) {
|
if (name !== company.name) {
|
||||||
const taken = await byName(name, tx);
|
const taken = await byName(name, runner);
|
||||||
if (taken) {
|
if (taken) {
|
||||||
throw new Error('Company name not available');
|
throw new Error('Company name not available');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageId = await ensureLang(iso639, tx);
|
const languageId = await ensureLang(iso639, runner);
|
||||||
const taxrateId = await ensureTax(taxrate, tx);
|
const taxrateId = await ensureTax(taxrate, runner);
|
||||||
const reducedtaxrateId = await ensureTax(reducedtaxrate, tx);
|
const reducedtaxrateId = await ensureTax(reducedtaxrate, runner);
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner.query(updateSql, {
|
||||||
.input('id', id)
|
id,
|
||||||
.input('name', name)
|
name,
|
||||||
.input('languageId', languageId)
|
languageId,
|
||||||
.input('taxrate', taxrateId)
|
taxrate: taxrateId,
|
||||||
.input('reducedtaxrate', reducedtaxrateId)
|
reducedtaxrate: reducedtaxrateId,
|
||||||
.query(UPDATE);
|
});
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -66,13 +65,14 @@ export default function update(pool) {
|
|||||||
reducedtaxrateId,
|
reducedtaxrateId,
|
||||||
taxfreerateId: company.taxfreerate,
|
taxfreerateId: company.taxfreerate,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runUpdate(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runUpdate);
|
||||||
|
}
|
||||||
|
return runUpdate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { GET_BY_COMPANY_AND_KEY } from '../../providers/mssql/queries/companysetting.js';
|
import { GET_BY_COMPANY_AND_KEY as MSSQL_GET_BY_COMPANY_AND_KEY } from '../../providers/mssql/queries/companysetting.js';
|
||||||
|
import { GET_BY_COMPANY_AND_KEY as SQLITE_GET_BY_COMPANY_AND_KEY } from '../../providers/sqlite3/queries/companysetting.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shapeSetting from './shapeSetting.js';
|
import shapeSetting from './shapeSetting.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function get(pool) {
|
export default function get(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_KEY
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_KEY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a single company setting by key for the user's selected company.
|
* Returns a single company setting by key for the user's selected company.
|
||||||
@@ -13,6 +19,7 @@ export default function get(pool) {
|
|||||||
* @param {object} [options] - Optional options.
|
* @param {object} [options] - Optional options.
|
||||||
* @param {boolean} [options.frontendOnly] - Only return the setting if it is frontend-visible.
|
* @param {boolean} [options.frontendOnly] - Only return the setting if it is frontend-visible.
|
||||||
* @param {boolean} [options.maskWriteonly] - Mask writeonly settings as '***'. Defaults to `frontendOnly` (usergui gets '***', backend gets full value).
|
* @param {boolean} [options.maskWriteonly] - Mask writeonly settings as '***'. Defaults to `frontendOnly` (usergui gets '***', backend gets full value).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}|undefined>} The setting, or `undefined` if not found.
|
* @returns {Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}|undefined>} The setting, or `undefined` if not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -23,20 +30,20 @@ export default function get(pool) {
|
|||||||
googleAuthId,
|
googleAuthId,
|
||||||
key,
|
key,
|
||||||
{ frontendOnly = false, maskWriteonly } = {},
|
{ frontendOnly = false, maskWriteonly } = {},
|
||||||
|
tx,
|
||||||
) => {
|
) => {
|
||||||
if (!key?.trim()) {
|
if (!key?.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
key: key.trim(),
|
||||||
.input('key', key.trim())
|
});
|
||||||
.query(GET_BY_COMPANY_AND_KEY);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
LIST_BY_COMPANY,
|
LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY,
|
||||||
LIST_BY_COMPANY_FRONTEND,
|
LIST_BY_COMPANY_FRONTEND as MSSQL_LIST_BY_COMPANY_FRONTEND,
|
||||||
} from '../../providers/mssql/queries/companysetting.js';
|
} from '../../providers/mssql/queries/companysetting.js';
|
||||||
|
import {
|
||||||
|
LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY,
|
||||||
|
LIST_BY_COMPANY_FRONTEND as SQLITE_LIST_BY_COMPANY_FRONTEND,
|
||||||
|
} from '../../providers/sqlite3/queries/companysetting.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shapeSetting from './shapeSetting.js';
|
import shapeSetting from './shapeSetting.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const listSql = isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY;
|
||||||
|
const listFrontendSql = isSqlite
|
||||||
|
? SQLITE_LIST_BY_COMPANY_FRONTEND
|
||||||
|
: MSSQL_LIST_BY_COMPANY_FRONTEND;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the settings of the user's selected company, optionally only frontend-visible ones.
|
* Lists the settings of the user's selected company, optionally only frontend-visible ones.
|
||||||
@@ -18,6 +28,7 @@ export default function list(pool) {
|
|||||||
* @param {object} [options] - Optional options.
|
* @param {object} [options] - Optional options.
|
||||||
* @param {boolean} [options.frontendOnly] - Only return frontend-visible settings.
|
* @param {boolean} [options.frontendOnly] - Only return frontend-visible settings.
|
||||||
* @param {boolean} [options.maskWriteonly] - Whether to mask writeonly settings with '***'. Defaults to `true`.
|
* @param {boolean} [options.maskWriteonly] - Whether to mask writeonly settings with '***'. Defaults to `true`.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>} The settings.
|
* @returns {Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>} The settings.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -27,15 +38,16 @@ export default function list(pool) {
|
|||||||
return async (
|
return async (
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
{ frontendOnly = false, maskWriteonly = true } = {},
|
{ frontendOnly = false, maskWriteonly = true } = {},
|
||||||
|
tx,
|
||||||
) => {
|
) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(
|
||||||
.request()
|
frontendOnly ? listFrontendSql : listSql,
|
||||||
.input('companyId', companyId)
|
{ companyId },
|
||||||
.query(frontendOnly ? LIST_BY_COMPANY_FRONTEND : LIST_BY_COMPANY);
|
);
|
||||||
|
|
||||||
return result.recordset.map((row) =>
|
return result.rows.map((row) =>
|
||||||
shapeSetting(row, { exposeValue: !maskWriteonly }),
|
shapeSetting(row, { exposeValue: !maskWriteonly }),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../providers/mssql/queries/companysetting.js';
|
import {
|
||||||
|
GET_BY_COMPANY_AND_KEY as MSSQL_GET_BY_COMPANY_AND_KEY,
|
||||||
|
UPSERT as MSSQL_UPSERT,
|
||||||
|
} from '../../providers/mssql/queries/companysetting.js';
|
||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_KEY as SQLITE_GET_BY_COMPANY_AND_KEY,
|
||||||
|
UPSERT as SQLITE_UPSERT,
|
||||||
|
} from '../../providers/sqlite3/queries/companysetting.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shapeSetting from './shapeSetting.js';
|
import shapeSetting from './shapeSetting.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
const ALLOWED_KEYS = new Set([
|
const ALLOWED_KEYS = new Set([
|
||||||
'ui.theme',
|
'ui.theme',
|
||||||
@@ -10,32 +18,29 @@ const ALLOWED_KEYS = new Set([
|
|||||||
'labelprinter.type',
|
'labelprinter.type',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Keys that are write-only by default: the GUI receives a scrambled "***"
|
|
||||||
// placeholder when reading, but the backend always gets the real value.
|
|
||||||
const WRITEONLY_KEYS = new Set([
|
const WRITEONLY_KEYS = new Set([
|
||||||
'keys.tavily',
|
'keys.tavily',
|
||||||
'keys.openrouter',
|
'keys.openrouter',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default function set(pool) {
|
export default function set(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_KEY
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_KEY;
|
||||||
|
const upsertSql = isSqlite ? SQLITE_UPSERT : MSSQL_UPSERT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets (upserts) a company setting by key for the user's selected company.
|
* Sets (upserts) a company setting by key for the user's selected company.
|
||||||
*
|
*
|
||||||
* Only a fixed allow-list of keys can be set: `ui.theme`, `keys.tavily`,
|
|
||||||
* `keys.openrouter`, `labelprinter.addr`, `labelprinter.type`.
|
|
||||||
*
|
|
||||||
* Keys in the write-only set (`keys.tavily`, `keys.openrouter`) are stored
|
|
||||||
* with `writeonly = 1`; when retrieved by the GUI the value is returned as
|
|
||||||
* `"***"`. The backend always receives the real value.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {object} input - The setting input.
|
* @param {object} input - The setting input.
|
||||||
* @param {string} input.key - The setting key (must be in the allow-list).
|
* @param {string} input.key - The setting key (must be in the allow-list).
|
||||||
* @param {string} input.value - The setting value.
|
* @param {string} input.value - The setting value.
|
||||||
* @param {boolean} [input.frontend] - Whether the setting is frontend-visible.
|
* @param {boolean} [input.frontend] - Whether the setting is frontend-visible.
|
||||||
* @param {boolean} [input.writeonly] - Whether the setting is write-only. Defaults to true for `keys.tavily` and `keys.openrouter`, false otherwise.
|
* @param {boolean} [input.writeonly] - Whether the setting is write-only. Defaults to true for `keys.tavily` and `keys.openrouter`, false otherwise.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>} The upserted setting.
|
* @returns {Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>} The upserted setting.
|
||||||
* @throws {Error} If the input is invalid.
|
* @throws {Error} If the input is invalid.
|
||||||
*
|
*
|
||||||
@@ -43,7 +48,7 @@ export default function set(pool) {
|
|||||||
* const setting = await store.companysetting.set(42, { key: 'ui.theme', value: 'dark' });
|
* const setting = await store.companysetting.set(42, { key: 'ui.theme', value: 'dark' });
|
||||||
* // => { key, value, frontend, writeonly }
|
* // => { key, value, frontend, writeonly }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, input = {}) => {
|
return async (googleAuthId, input = {}, tx) => {
|
||||||
const key = input.key?.trim();
|
const key = input.key?.trim();
|
||||||
if (!ALLOWED_KEYS.has(key)) {
|
if (!ALLOWED_KEYS.has(key)) {
|
||||||
throw new Error('Invalid setting');
|
throw new Error('Invalid setting');
|
||||||
@@ -52,29 +57,25 @@ export default function set(pool) {
|
|||||||
throw new Error('Invalid setting');
|
throw new Error('Invalid setting');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const frontend = Boolean(input.frontend);
|
const frontend = Boolean(input.frontend);
|
||||||
const writeonly =
|
const writeonly =
|
||||||
input.writeonly !== undefined
|
input.writeonly !== undefined
|
||||||
? Boolean(input.writeonly)
|
? Boolean(input.writeonly)
|
||||||
: WRITEONLY_KEYS.has(key);
|
: WRITEONLY_KEYS.has(key);
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
await pool
|
await runner.query(upsertSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
key,
|
||||||
.input('key', key)
|
value: input.value,
|
||||||
.input('value', input.value)
|
frontend: isSqlite ? (frontend ? 1 : 0) : frontend,
|
||||||
.input('frontend', frontend)
|
writeonly: isSqlite ? (writeonly ? 1 : 0) : writeonly,
|
||||||
.input('writeonly', writeonly)
|
});
|
||||||
.query(UPSERT);
|
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(getSql, { companyId, key });
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.input('key', key)
|
|
||||||
.query(GET_BY_COMPANY_AND_KEY);
|
|
||||||
|
|
||||||
// Return the masked value to the GUI; the DB holds the real value.
|
// Return the masked value to the GUI; the DB holds the real value.
|
||||||
return shapeSetting(result.recordset[0]);
|
return shapeSetting(result.rows[0]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { ADD } from '../../providers/mssql/queries/companyuser.js';
|
import { ADD as MSSQL_ADD } from '../../providers/mssql/queries/companyuser.js';
|
||||||
|
import { ADD as SQLITE_ADD } from '../../providers/sqlite3/queries/companyuser.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function add(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_ADD : MSSQL_ADD;
|
||||||
|
|
||||||
export default function add(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Adds a company user linking a company to a Google auth account.
|
* Adds a company user linking a company to a Google auth account.
|
||||||
*
|
*
|
||||||
* @param {number} companyId - The id of the company.
|
* @param {number} companyId - The id of the company.
|
||||||
* @param {number} googleAuthId - The id of the Google auth account.
|
* @param {number} googleAuthId - The id of the Google auth account.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<number>} The id of the created company user link.
|
* @returns {Promise<number>} The id of the created company user link.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const id = await store.companyuser.add(1, 42);
|
* const id = await store.companyuser.add(1, 42);
|
||||||
* // => 7
|
* // => 7
|
||||||
*/
|
*/
|
||||||
return async (companyId, googleAuthId) => {
|
return async (companyId, googleAuthId, tx) => {
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
googleAuthId,
|
||||||
.input('googleAuthId', googleAuthId)
|
});
|
||||||
.query(ADD);
|
|
||||||
|
|
||||||
return result.recordset[0].id;
|
return result.rows[0]?.id ?? result.insertId;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { DELETE } from '../../providers/mssql/queries/companyuser.js';
|
import { DELETE as MSSQL_DELETE } from '../../providers/mssql/queries/companyuser.js';
|
||||||
|
import { DELETE as SQLITE_DELETE } from '../../providers/sqlite3/queries/companyuser.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function del(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
export default function del(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Deletes a company user link by its id.
|
* Deletes a company user link by its id.
|
||||||
*
|
*
|
||||||
* @param {number} id - The id of the company user link to delete.
|
* @param {number} id - The id of the company user link to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.companyuser.delete(7);
|
* await store.companyuser.delete(7);
|
||||||
*/
|
*/
|
||||||
return async (id) => {
|
return async (id, tx) => {
|
||||||
await pool
|
await makeRunner(provider, tx).query(sql, { id });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.query(DELETE);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import { GET_BY_EMAIL } from '../../providers/mssql/queries/companyuser.js';
|
import { GET_BY_EMAIL as MSSQL_GET_BY_EMAIL } from '../../providers/mssql/queries/companyuser.js';
|
||||||
|
import { GET_BY_EMAIL as SQLITE_GET_BY_EMAIL } from '../../providers/sqlite3/queries/companyuser.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function getByEmail(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_GET_BY_EMAIL
|
||||||
|
: MSSQL_GET_BY_EMAIL;
|
||||||
|
|
||||||
export default function getByEmail(pool) {
|
|
||||||
/**
|
/**
|
||||||
* Returns the company user ids associated with the given email address.
|
* Returns the company user ids associated with the given email address.
|
||||||
*
|
*
|
||||||
* @param {string} email - The email address.
|
* @param {string} email - The email address.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<number>>} The company user ids.
|
* @returns {Promise<Array<number>>} The company user ids.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const ids = await store.companyuser.getByEmail('user@example.com');
|
* const ids = await store.companyuser.getByEmail('user@example.com');
|
||||||
* // => [1, 2, 3]
|
* // => [1, 2, 3]
|
||||||
*/
|
*/
|
||||||
return async (email) => {
|
return async (email, tx) => {
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, { email });
|
||||||
.request()
|
|
||||||
.input('email', email)
|
|
||||||
.query(GET_BY_EMAIL);
|
|
||||||
|
|
||||||
return result.recordset.map((row) => row.id);
|
return result.rows.map((row) => row.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { GET_COMPANY_ACCESS } from '../../providers/mssql/queries/companyuser.js';
|
import { GET_COMPANY_ACCESS as MSSQL_GET_COMPANY_ACCESS } from '../../providers/mssql/queries/companyuser.js';
|
||||||
|
import { GET_COMPANY_ACCESS as SQLITE_GET_COMPANY_ACCESS } from '../../providers/sqlite3/queries/companyuser.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function resolveCompanyAccess(pool) {
|
export default function resolveCompanyAccess(provider) {
|
||||||
return async (googleAuthId, companyId) => {
|
const sql =
|
||||||
const result = await pool
|
provider?.dialect === 'sqlite3'
|
||||||
.request()
|
? SQLITE_GET_COMPANY_ACCESS
|
||||||
.input('googleAuthId', googleAuthId)
|
: MSSQL_GET_COMPANY_ACCESS;
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_COMPANY_ACCESS);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
return async (googleAuthId, companyId, tx) => {
|
||||||
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
|
googleAuthId,
|
||||||
|
companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Company not found');
|
throw new Error('Company not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { GET_BY_ID } from '../../providers/mssql/queries/google_auth.js';
|
import { GET_BY_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/google_auth.js';
|
||||||
|
import { GET_BY_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/google_auth.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
import resolveCompanyAccess from './resolveCompanyAccess.js';
|
import resolveCompanyAccess from './resolveCompanyAccess.js';
|
||||||
|
|
||||||
export default function resolveSelectedCompanyAccess(pool) {
|
export default function resolveSelectedCompanyAccess(provider) {
|
||||||
const access = resolveCompanyAccess(pool);
|
const access = resolveCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the user's selected company and checks company membership.
|
* Resolves the user's selected company and checks company membership.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{companyId: number, rootCategoryId: number, languageId: number, posCustomerGroupId: number|null, companyuserId: number}>} The resolved company access context.
|
* @returns {Promise<{companyId: number, rootCategoryId: number, languageId: number, posCustomerGroupId: number|null, companyuserId: number}>} The resolved company access context.
|
||||||
* @throws {Error} If the user is not authorized or has no company selected.
|
* @throws {Error} If the user is not authorized or has no company selected.
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
if (!Number.isFinite(googleAuthId)) {
|
if (!Number.isFinite(googleAuthId)) {
|
||||||
throw new Error('Not authorized');
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, { googleAuthId });
|
||||||
.request()
|
|
||||||
.input('googleAuthId', googleAuthId)
|
|
||||||
.query(GET_BY_ID);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Not authorized');
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
@@ -31,7 +33,7 @@ export default function resolveSelectedCompanyAccess(pool) {
|
|||||||
throw new Error('No company selected');
|
throw new Error('No company selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = await access(googleAuthId, companyId);
|
const ctx = await access(googleAuthId, companyId, tx);
|
||||||
return { companyId, ...ctx };
|
return { companyId, ...ctx };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { CREATE, GET_NEXT_SORT } from '../../providers/mssql/queries/customergroup.js';
|
import {
|
||||||
|
CREATE as MSSQL_CREATE,
|
||||||
|
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/mssql/queries/customergroup.js';
|
||||||
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
const RESERVED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
|
const RESERVED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
|
||||||
|
|
||||||
@@ -8,8 +16,11 @@ function shape(row) {
|
|||||||
return { id: row.id, name: row.name, sort: row.sort };
|
return { id: row.id, name: row.name, sort: row.sort };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new customer group for the user's selected company.
|
* Creates a new customer group for the user's selected company.
|
||||||
@@ -17,6 +28,7 @@ export default function create(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {object} input - The customer group input.
|
* @param {object} input - The customer group input.
|
||||||
* @param {string} input.name - The customer group name (must not be a reserved name).
|
* @param {string} input.name - The customer group name (must not be a reserved name).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number}>} The created customer group.
|
* @returns {Promise<{id: number, name: string, sort: number}>} The created customer group.
|
||||||
* @throws {Error} If the name is empty or reserved.
|
* @throws {Error} If the name is empty or reserved.
|
||||||
*
|
*
|
||||||
@@ -24,7 +36,7 @@ export default function create(pool) {
|
|||||||
* const group = await store.customergroup.create(42, { name: 'Wholesale' });
|
* const group = await store.customergroup.create(42, { name: 'Wholesale' });
|
||||||
* // => { id, name, sort }
|
* // => { id, name, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, { name } = {}) => {
|
return async (googleAuthId, { name } = {}, tx) => {
|
||||||
const cleaned = name?.trim();
|
const cleaned = name?.trim();
|
||||||
if (!cleaned) {
|
if (!cleaned) {
|
||||||
throw new Error('Name required');
|
throw new Error('Name required');
|
||||||
@@ -33,21 +45,19 @@ export default function create(pool) {
|
|||||||
throw new Error('Name not allowed');
|
throw new Error('Name not allowed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const next = await pool
|
const next = await runner.query(getSortSql, { companyId });
|
||||||
.request()
|
const sort = next.rows[0].sort;
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_NEXT_SORT);
|
|
||||||
const sort = next.recordset[0].sort;
|
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(createSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
name: cleaned,
|
||||||
.input('name', cleaned)
|
sort,
|
||||||
.input('sort', sort)
|
});
|
||||||
.query(CREATE);
|
|
||||||
|
|
||||||
return shape(result.recordset[0]);
|
const row = result.rows[0] ?? { id: result.insertId, name: cleaned, sort };
|
||||||
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import { DELETE_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/customergroup.js';
|
import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/customergroup.js';
|
||||||
|
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_DELETE_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_DELETE_BY_COMPANY_AND_ID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a customer group for the user's selected company by its id.
|
* Deletes a customer group for the user's selected company by its id.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The id of the customer group to delete.
|
* @param {number} id - The id of the customer group to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the customer group is not found.
|
* @throws {Error} If the customer group is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.customergroup.delete(42, 5);
|
* await store.customergroup.delete(42, 5);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(DELETE_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.number === 547) {
|
if (isForeignKeyConstraintError(err)) {
|
||||||
throw new Error('Customer group in use');
|
throw new Error('Customer group in use');
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
import { LIST_BY_COMPANY } from '../../providers/mssql/queries/customergroup.js';
|
import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/customergroup.js';
|
||||||
|
import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_LIST_BY_COMPANY
|
||||||
|
: MSSQL_LIST_BY_COMPANY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the customer groups of the user's selected company.
|
* Lists the customer groups of the user's selected company.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{id: number, name: string, sort: number}>>} The customer groups.
|
* @returns {Promise<Array<{id: number, name: string, sort: number}>>} The customer groups.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const groups = await store.customergroup.list(42);
|
* const groups = await store.customergroup.list(42);
|
||||||
* // => [{ id, name, sort }]
|
* // => [{ id, name, sort }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, { companyId });
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(LIST_BY_COMPANY);
|
|
||||||
|
|
||||||
return result.recordset.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
sort: row.sort,
|
sort: row.sort,
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
LIST_IDS_BY_COMPANY,
|
LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS_BY_COMPANY,
|
||||||
UPDATE_SORT,
|
UPDATE_SORT as MSSQL_UPDATE_SORT,
|
||||||
} from '../../providers/mssql/queries/customergroup.js';
|
} from '../../providers/mssql/queries/customergroup.js';
|
||||||
import request from '../request.js';
|
import {
|
||||||
|
LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS_BY_COMPANY,
|
||||||
|
UPDATE_SORT as SQLITE_UPDATE_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function order(pool) {
|
export default function order(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const listIdsSql = isSqlite
|
||||||
|
? SQLITE_LIST_IDS_BY_COMPANY
|
||||||
|
: MSSQL_LIST_IDS_BY_COMPANY;
|
||||||
|
const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the customer groups of the user's selected company.
|
* Reorders the customer groups of the user's selected company.
|
||||||
@@ -14,6 +23,7 @@ export default function order(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {object} input - The reorder input.
|
* @param {object} input - The reorder input.
|
||||||
* @param {Array<number>} input.customergroupIds - The customer group ids in their new order.
|
* @param {Array<number>} input.customergroupIds - The customer group ids in their new order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{customergroupIds: Array<number>}>} The applied order.
|
* @returns {Promise<{customergroupIds: Array<number>}>} The applied order.
|
||||||
* @throws {Error} If the order is invalid.
|
* @throws {Error} If the order is invalid.
|
||||||
*
|
*
|
||||||
@@ -21,58 +31,51 @@ export default function order(pool) {
|
|||||||
* const result = await store.customergroup.order(42, { customergroupIds: [3, 1, 2] });
|
* const result = await store.customergroup.order(42, { customergroupIds: [3, 1, 2] });
|
||||||
* // => { customergroupIds: [3, 1, 2] }
|
* // => { customergroupIds: [3, 1, 2] }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, { customergroupIds } = {}) => {
|
return async (googleAuthId, { customergroupIds } = {}, tx) => {
|
||||||
if (!Array.isArray(customergroupIds)) {
|
if (!Array.isArray(customergroupIds)) {
|
||||||
throw new Error('Invalid customer group order');
|
throw new Error('Invalid customer group order');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runOrder = async (runner) => {
|
||||||
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
const groups = await pool
|
const groups = await runner.query(listIdsSql, { companyId });
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(LIST_IDS_BY_COMPANY);
|
|
||||||
|
|
||||||
const existing = new Set(groups.recordset.map((r) => r.id));
|
const existing = new Set(groups.rows.map((r) => r.id));
|
||||||
|
|
||||||
if (customergroupIds.length !== existing.size) {
|
if (customergroupIds.length !== existing.size) {
|
||||||
throw new Error('Invalid customer group order');
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set();
|
|
||||||
for (const customergroupId of customergroupIds) {
|
|
||||||
if (
|
|
||||||
!Number.isFinite(customergroupId) ||
|
|
||||||
!existing.has(customergroupId) ||
|
|
||||||
seen.has(customergroupId)
|
|
||||||
) {
|
|
||||||
throw new Error('Invalid customer group order');
|
throw new Error('Invalid customer group order');
|
||||||
}
|
}
|
||||||
seen.add(customergroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const seen = new Set();
|
||||||
await tx.begin();
|
for (const customergroupId of customergroupIds) {
|
||||||
|
if (
|
||||||
try {
|
!Number.isFinite(customergroupId) ||
|
||||||
for (let sort = 0; sort < customergroupIds.length; sort++) {
|
!existing.has(customergroupId) ||
|
||||||
await request(pool, tx)
|
seen.has(customergroupId)
|
||||||
.input('id', customergroupIds[sort])
|
) {
|
||||||
.input('companyId', companyId)
|
throw new Error('Invalid customer group order');
|
||||||
.input('sort', sort)
|
}
|
||||||
.query(UPDATE_SORT);
|
seen.add(customergroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx.commit();
|
for (let sort = 0; sort < customergroupIds.length; sort++) {
|
||||||
|
await runner.query(updateSortSql, {
|
||||||
|
id: customergroupIds[sort],
|
||||||
|
companyId,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { customergroupIds };
|
return { customergroupIds };
|
||||||
} catch (err) {
|
};
|
||||||
try {
|
|
||||||
await tx.rollback();
|
if (tx) {
|
||||||
} catch {
|
return runOrder(makeRunner(provider, tx));
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runOrder);
|
||||||
|
}
|
||||||
|
return runOrder(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/customergroup.js';
|
import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/customergroup.js';
|
||||||
import request from '../request.js';
|
import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
/** Ensure customergroup belongs to company. Throws Customer group not found. */
|
/** Ensure customergroup belongs to company. Throws Customer group not found. */
|
||||||
export default function resolveCustomerGroupAccess(pool) {
|
export default function resolveCustomerGroupAccess(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_ID;
|
||||||
|
|
||||||
return async (companyId, customergroupId, tx) => {
|
return async (companyId, customergroupId, tx) => {
|
||||||
if (!Number.isFinite(customergroupId)) {
|
if (!Number.isFinite(customergroupId)) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.input('id', customergroupId)
|
id: customergroupId,
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.query(GET_BY_COMPANY_AND_ID);
|
});
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../providers/mssql/queries/customergroup.js';
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_COMPANY_AND_ID,
|
||||||
|
UPDATE as MSSQL_UPDATE,
|
||||||
|
} from '../../providers/mssql/queries/customergroup.js';
|
||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_COMPANY_AND_ID,
|
||||||
|
UPDATE as SQLITE_UPDATE,
|
||||||
|
} from '../../providers/sqlite3/queries/customergroup.js';
|
||||||
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
const LOCKED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
|
const LOCKED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]);
|
||||||
|
|
||||||
@@ -8,8 +16,13 @@ function shape(row) {
|
|||||||
return { id: row.id, name: row.name, sort: row.sort };
|
return { id: row.id, name: row.name, sort: row.sort };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_ID;
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the name of a customer group for the user's selected company.
|
* Updates the name of a customer group for the user's selected company.
|
||||||
@@ -18,6 +31,7 @@ export default function update(pool) {
|
|||||||
* @param {number} id - The id of the customer group to update.
|
* @param {number} id - The id of the customer group to update.
|
||||||
* @param {object} input - The update input.
|
* @param {object} input - The update input.
|
||||||
* @param {string} input.name - The new customer group name.
|
* @param {string} input.name - The new customer group name.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, name: string, sort: number}>} The updated customer group.
|
* @returns {Promise<{id: number, name: string, sort: number}>} The updated customer group.
|
||||||
* @throws {Error} If the customer group is not found or the name is empty.
|
* @throws {Error} If the customer group is not found or the name is empty.
|
||||||
*
|
*
|
||||||
@@ -25,7 +39,7 @@ export default function update(pool) {
|
|||||||
* const group = await store.customergroup.update(42, 5, { name: 'Retail' });
|
* const group = await store.customergroup.update(42, 5, { name: 'Retail' });
|
||||||
* // => { id, name, sort }
|
* // => { id, name, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, { name } = {}) => {
|
return async (googleAuthId, id, { name } = {}, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
@@ -35,15 +49,12 @@ export default function update(pool) {
|
|||||||
throw new Error('Name required');
|
throw new Error('Name required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const existing = await pool
|
const existing = await runner.query(getSql, { id, companyId });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
const current = existing.recordset[0];
|
const current = existing.rows[0];
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Customer group not found');
|
throw new Error('Customer group not found');
|
||||||
}
|
}
|
||||||
@@ -52,17 +63,13 @@ export default function update(pool) {
|
|||||||
throw new Error('Name cannot be changed');
|
throw new Error('Name cannot be changed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(updateSql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
name: cleaned,
|
||||||
.input('name', cleaned)
|
});
|
||||||
.query(UPDATE);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0] ?? { id, name: cleaned, sort: current.sort };
|
||||||
if (!row) {
|
|
||||||
throw new Error('Customer group not found');
|
|
||||||
}
|
|
||||||
return shape(row);
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { LIST_BY_MANIFEST } from '../../providers/mssql/queries/manifestattachment.js';
|
import * as mssqlManifestAttachmentQueries from '../../providers/mssql/queries/manifestattachment.js';
|
||||||
import request from '../request.js';
|
import * as sqliteManifestAttachmentQueries from '../../providers/sqlite3/queries/manifestattachment.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
import generateDocumentThumb from '../../util/generateDocumentThumb.js';
|
import generateDocumentThumb from '../../util/generateDocumentThumb.js';
|
||||||
import { parseMeta } from '../manifestattachment/listByManifestId.js';
|
import { parseMeta } from '../manifestattachment/listByManifestId.js';
|
||||||
|
|
||||||
@@ -33,12 +34,25 @@ export function shapeAttachmentMeta(row) {
|
|||||||
/**
|
/**
|
||||||
* Loads attachment metadata (without binary content) for a manifest.
|
* Loads attachment metadata (without binary content) for a manifest.
|
||||||
*/
|
*/
|
||||||
export async function loadAttachments(pool, tx, manifestId) {
|
export async function loadAttachments(provider, tx, manifestId) {
|
||||||
const result = await request(pool, tx)
|
const runner = makeRunner(provider, tx);
|
||||||
.input('manifestId', manifestId)
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
.query(LIST_BY_MANIFEST);
|
const queries = isSqlite ? sqliteManifestAttachmentQueries : mssqlManifestAttachmentQueries;
|
||||||
|
|
||||||
return result.recordset.map(shapeAttachmentMeta);
|
const result = await runner(queries.LIST_BY_MANIFEST, { manifestId });
|
||||||
|
const rows = await Promise.all(
|
||||||
|
result.rows.map(async (row) => {
|
||||||
|
let thumbavif = row.thumbavif;
|
||||||
|
if (isSqlite) {
|
||||||
|
thumbavif = await (runner.readThumbAvif ? runner.readThumbAvif(row.hash) : provider?.readThumbAvif?.(row.hash));
|
||||||
|
}
|
||||||
|
return shapeAttachmentMeta({
|
||||||
|
...row,
|
||||||
|
thumbavif,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAttachment(raw, index) {
|
function normalizeAttachment(raw, index) {
|
||||||
@@ -57,25 +71,26 @@ function normalizeAttachment(raw, index) {
|
|||||||
throw new Error('Invalid file');
|
throw new Error('Invalid file');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw.content == null) {
|
if (rawContent(raw) == null) {
|
||||||
throw new Error('Invalid file');
|
throw new Error('Invalid file');
|
||||||
}
|
}
|
||||||
let content;
|
let content;
|
||||||
if (Buffer.isBuffer(raw.content)) {
|
const rawData = rawContent(raw);
|
||||||
content = raw.content;
|
if (Buffer.isBuffer(rawData)) {
|
||||||
} else if (typeof raw.content === 'string') {
|
content = rawData;
|
||||||
const trimmed = raw.content.trim();
|
} else if (typeof rawData === 'string') {
|
||||||
|
const trimmed = rawData.trim();
|
||||||
if (mimetype === 'application/pdf' || trimmed.startsWith('JVBER') || /^[A-Za-z0-9+/=]+$/.test(trimmed.slice(0, 100))) {
|
if (mimetype === 'application/pdf' || trimmed.startsWith('JVBER') || /^[A-Za-z0-9+/=]+$/.test(trimmed.slice(0, 100))) {
|
||||||
try {
|
try {
|
||||||
content = Buffer.from(trimmed, 'base64');
|
content = Buffer.from(trimmed, 'base64');
|
||||||
} catch {
|
} catch {
|
||||||
content = Buffer.from(raw.content, 'utf8');
|
content = Buffer.from(rawData, 'utf8');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = Buffer.from(raw.content, 'utf8');
|
content = Buffer.from(rawData, 'utf8');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = Buffer.from(raw.content);
|
content = Buffer.from(rawData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = Number.isFinite(raw.size) && raw.size >= 0
|
const size = Number.isFinite(raw.size) && raw.size >= 0
|
||||||
@@ -126,54 +141,80 @@ function normalizeAttachment(raw, index) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rawContent(raw) {
|
||||||
|
return raw.content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts pdf/txt attachments for a delivery (manifest) inside an existing
|
* Inserts pdf/txt attachments for a delivery (manifest) inside an existing
|
||||||
* transaction. Sort orders continue after the current maximum.
|
* transaction. Sort orders continue after the current maximum.
|
||||||
*
|
*
|
||||||
* @param {object} pool
|
* @param {object} provider
|
||||||
* @param {object} tx - Active transaction.
|
* @param {object} tx - Active transaction.
|
||||||
* @param {object} sql - SQL statements from delivery.js.
|
* @param {object} sql - SQL statements from delivery.js.
|
||||||
* @param {number} manifestId
|
* @param {number} manifestId
|
||||||
* @param {Array<{mimetype: string, name: string, content: Buffer, sort?: number, size?: number, created?: Date|string, modified?: Date|string, hash?: string}>} [files]
|
* @param {Array<{mimetype: string, name: string, content: Buffer, sort?: number, size?: number, created?: Date|string, modified?: Date|string, hash?: string}>} [files]
|
||||||
* @returns {Promise<Array<object>>} Attachment metadata rows.
|
* @returns {Promise<Array<object>>} Attachment metadata rows.
|
||||||
*/
|
*/
|
||||||
export async function insertAttachments(pool, tx, sql, manifestId, files) {
|
export async function insertAttachments(provider, tx, sql, manifestId, files) {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
const attachments = (files ?? []).map(normalizeAttachment);
|
const attachments = (files ?? []).map(normalizeAttachment);
|
||||||
|
|
||||||
const sortRes = await request(pool, tx)
|
const sortRes = await runner(sql.GET_NEXT_ATTACHMENT_SORT, { manifestId });
|
||||||
.input('manifestId', manifestId)
|
let nextSort = sortRes.rows[0].sort;
|
||||||
.query(sql.GET_NEXT_ATTACHMENT_SORT);
|
|
||||||
let nextSort = sortRes.recordset[0].sort;
|
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const a of attachments) {
|
for (const a of attachments) {
|
||||||
const sort = a.sort ?? nextSort;
|
const sort = a.sort ?? nextSort;
|
||||||
nextSort = Math.max(nextSort, sort) + 1;
|
nextSort = Math.max(nextSort, sort) + 1;
|
||||||
|
|
||||||
// Generate thumbnail server-side (e.g. pdftocairo + sharp for PDF)
|
// Use client-provided thumbavif if present, otherwise generate server-side
|
||||||
const { thumbavif, thumbx, thumby } = await generateDocumentThumb(a.content, a.mimetype, { filename: a.name });
|
let thumbavif = a.thumbavif
|
||||||
|
? (Buffer.isBuffer(a.thumbavif)
|
||||||
|
? a.thumbavif
|
||||||
|
: Buffer.from(a.thumbavif, typeof a.thumbavif === 'string' ? 'base64' : undefined))
|
||||||
|
: null;
|
||||||
|
let thumbx = Number.isFinite(a.thumbx) ? Number(a.thumbx) : null;
|
||||||
|
let thumby = Number.isFinite(a.thumby) ? Number(a.thumby) : null;
|
||||||
|
|
||||||
|
if (!thumbavif) {
|
||||||
|
const generated = await generateDocumentThumb(a.content, a.mimetype, { filename: a.name });
|
||||||
|
thumbavif = generated.thumbavif;
|
||||||
|
thumbx = generated.thumbx;
|
||||||
|
thumby = generated.thumby;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider?.dialect === 'sqlite3') {
|
||||||
|
if (runner.writeFileContent) {
|
||||||
|
await runner.writeFileContent(a.hash, a.content, a.mimetype);
|
||||||
|
if (thumbavif) await runner.writeThumbAvif(a.hash, thumbavif);
|
||||||
|
} else if (provider?.writeFileContent) {
|
||||||
|
await provider.writeFileContent(a.hash, a.content, a.mimetype);
|
||||||
|
if (thumbavif) await provider.writeThumbAvif(a.hash, thumbavif);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fileRes = await request(pool, tx)
|
const fileRes = await runner(sql.CREATE_FILE, {
|
||||||
.input('hash', a.hash)
|
hash: a.hash,
|
||||||
.input('mimetype', a.mimetype)
|
mimetype: a.mimetype,
|
||||||
.input('name', a.name)
|
name: a.name,
|
||||||
.input('size', a.size)
|
size: a.size,
|
||||||
.input('created', a.created)
|
created: a.created,
|
||||||
.input('modified', a.modified)
|
modified: a.modified,
|
||||||
.input('content', a.content)
|
content: a.content,
|
||||||
.input('thumbavif', thumbavif)
|
thumbavif,
|
||||||
.input('thumbx', thumbx)
|
thumbx,
|
||||||
.input('thumby', thumby)
|
thumby,
|
||||||
.input('meta', a.meta === null ? null : JSON.stringify(a.meta))
|
meta: a.meta === null ? null : JSON.stringify(a.meta),
|
||||||
.query(sql.CREATE_FILE);
|
});
|
||||||
|
|
||||||
const fileId = fileRes.recordset[0].id;
|
const fileId = fileRes.rows[0]?.id ?? fileRes.insertId;
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner(sql.CREATE_ATTACHMENT, {
|
||||||
.input('manifestId', manifestId)
|
manifestId,
|
||||||
.input('fileId', fileId)
|
fileId,
|
||||||
.input('sort', sort)
|
sort,
|
||||||
.query(sql.CREATE_ATTACHMENT);
|
});
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
manifestId,
|
manifestId,
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
CREATE_MANIFEST,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
CREATE_FILE,
|
|
||||||
CREATE_ATTACHMENT,
|
|
||||||
GET_NEXT_ATTACHMENT_SORT,
|
|
||||||
DELIVERY_TYPE,
|
|
||||||
} from '../../providers/mssql/queries/delivery.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import nextNumber from '../numbers/nextNumber.js';
|
import nextNumber from '../numbers/nextNumber.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
@@ -13,30 +7,15 @@ import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
|||||||
import { loadDelivery } from './helpers.js';
|
import { loadDelivery } from './helpers.js';
|
||||||
import { insertAttachments } from './attachments.js';
|
import { insertAttachments } from './attachments.js';
|
||||||
import { assertSupplier } from './writeLines.js';
|
import { assertSupplier } from './writeLines.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
const allocateNumber = nextNumber(pool);
|
const allocateNumber = nextNumber(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new delivery for the user's selected company with its lines.
|
|
||||||
* The delivery number is allocated internally via `getnextnumberwithprefixandpadding`
|
|
||||||
* (`type` `delivery`); supply `externalNumber` for an external (supplier) reference.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @param {object} body - The delivery input.
|
|
||||||
* @param {string} body.date - The delivery date.
|
|
||||||
* @param {number} body.supplierId - The id of the supplier.
|
|
||||||
* @param {string} [body.externalNumber] - An external (supplier) reference number.
|
|
||||||
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [body.lines] - The delivery lines.
|
|
||||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created delivery.
|
|
||||||
* @throws {Error} If the delivery is invalid.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const delivery = await store.delivery.create(42, { date: '2026-01-01', supplierId: 5, lines: [...] });
|
|
||||||
* // => { id, number, type, entityId, externalNumber, date, lines: [...] }
|
|
||||||
*/
|
|
||||||
return async (googleAuthId, body = {}) => {
|
return async (googleAuthId, body = {}) => {
|
||||||
const date = parseDate(body.date);
|
const date = parseDate(body.date);
|
||||||
if (!date) {
|
if (!date) {
|
||||||
@@ -50,58 +29,51 @@ export default function create(pool) {
|
|||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const run = async (runner, tx) => {
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supplierId = await assertSupplier(
|
const supplierId = await assertSupplier(
|
||||||
pool,
|
provider,
|
||||||
tx,
|
tx,
|
||||||
companyId,
|
companyId,
|
||||||
body.supplierId ?? null,
|
body.supplierId ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const number = await allocateNumber(companyId, DELIVERY_TYPE, tx);
|
const number = await allocateNumber(companyId, queries.DELIVERY_TYPE, tx);
|
||||||
|
|
||||||
const created = await request(pool, tx)
|
const created = await runner(queries.CREATE_MANIFEST, {
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.input('number', number)
|
number,
|
||||||
.input('type', DELIVERY_TYPE)
|
type: queries.DELIVERY_TYPE,
|
||||||
.input('entityId', supplierId)
|
entityId: supplierId,
|
||||||
.input('externalNumber', externalNumber)
|
externalNumber,
|
||||||
.input('date', date)
|
date,
|
||||||
.query(CREATE_MANIFEST);
|
});
|
||||||
|
|
||||||
const manifestId = created.recordset[0].id;
|
const manifestId = created.rows[0]?.id ?? created.insertId;
|
||||||
|
|
||||||
await insertLines(pool, tx, {
|
await insertLines(provider, tx, {
|
||||||
manifestId,
|
manifestId,
|
||||||
lines,
|
lines,
|
||||||
ensureTax,
|
ensureTax,
|
||||||
});
|
});
|
||||||
|
|
||||||
await insertAttachments(
|
await insertAttachments(
|
||||||
pool,
|
provider,
|
||||||
tx,
|
tx,
|
||||||
{
|
{
|
||||||
GET_NEXT_ATTACHMENT_SORT,
|
GET_NEXT_ATTACHMENT_SORT: queries.GET_NEXT_ATTACHMENT_SORT,
|
||||||
CREATE_FILE,
|
CREATE_FILE: queries.CREATE_FILE,
|
||||||
CREATE_ATTACHMENT,
|
CREATE_ATTACHMENT: queries.CREATE_ATTACHMENT,
|
||||||
},
|
},
|
||||||
manifestId,
|
manifestId,
|
||||||
body.files,
|
body.files,
|
||||||
);
|
);
|
||||||
|
|
||||||
const delivery = await loadDelivery(pool, tx, companyId, manifestId);
|
return loadDelivery(provider, tx, companyId, manifestId);
|
||||||
await tx.commit();
|
};
|
||||||
return delivery;
|
|
||||||
} catch (err) {
|
if (typeof provider?.transaction === 'function') {
|
||||||
try {
|
return provider.transaction((runner, tx) => run(runner, tx));
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
import { DELETE_MANIFEST, DELIVERY_TYPE } from '../../providers/mssql/queries/delivery.js';
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a delivery by its id within the user's selected company.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @param {number} id - The id of the delivery to delete.
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
* @throws {Error} If the delivery is not found.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* await store.delivery.delete(42, 5);
|
|
||||||
*/
|
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId);
|
||||||
|
const runner = makeRunner(provider);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner(queries.DELETE_MANIFEST, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
type: queries.DELIVERY_TYPE,
|
||||||
.input('type', DELIVERY_TYPE)
|
});
|
||||||
.query(DELETE_MANIFEST);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
const rowsAffected = result.rowsAffected?.[0] ?? (typeof result.rowsAffected === 'number' ? result.rowsAffected : 0);
|
||||||
|
if (!rowsAffected) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import { loadDelivery } from './helpers.js';
|
import { loadDelivery } from './helpers.js';
|
||||||
|
|
||||||
export default function getById(pool) {
|
export default function getById(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a single delivery by its id within the user's selected company.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @param {number} id - The id of the delivery.
|
|
||||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The delivery.
|
|
||||||
* @throws {Error} If the delivery is not found.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const delivery = await store.delivery.getById(42, 5);
|
|
||||||
* // => { id, number, type, entityId, externalNumber, date, lines: [...] }
|
|
||||||
*/
|
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
@@ -23,7 +11,7 @@ export default function getById(pool) {
|
|||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId);
|
||||||
|
|
||||||
const delivery = await loadDelivery(pool, null, companyId, id);
|
const delivery = await loadDelivery(provider, null, companyId, id);
|
||||||
if (!delivery) {
|
if (!delivery) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
DELIVERY_TYPE,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
GET_DELIVERY_HEADER,
|
import makeRunner from '../runner.js';
|
||||||
GET_DELIVERY_LINES,
|
|
||||||
} from '../../providers/mssql/queries/delivery.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import { shapeLine } from '../manifestLines.js';
|
import { shapeLine } from '../manifestLines.js';
|
||||||
import { loadAttachments } from './attachments.js';
|
import { loadAttachments } from './attachments.js';
|
||||||
|
|
||||||
|
export const DELIVERY_TYPE = mssqlQueries.DELIVERY_TYPE;
|
||||||
|
|
||||||
export function shapeSummary(row) {
|
export function shapeSummary(row) {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -18,27 +17,31 @@ export function shapeSummary(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadDelivery(pool, tx, companyId, id) {
|
export async function loadDelivery(provider, tx, companyId, id) {
|
||||||
const header = await request(pool, tx)
|
const runner = makeRunner(provider, tx);
|
||||||
.input('id', id)
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
.input('companyId', companyId)
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
.input('type', DELIVERY_TYPE)
|
|
||||||
.query(GET_DELIVERY_HEADER);
|
|
||||||
|
|
||||||
const row = header.recordset[0];
|
const header = await runner(queries.GET_DELIVERY_HEADER, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: DELIVERY_TYPE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = header.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = await request(pool, tx)
|
const lines = await runner(queries.GET_DELIVERY_LINES, {
|
||||||
.input('manifestId', id)
|
manifestId: id,
|
||||||
.query(GET_DELIVERY_LINES);
|
});
|
||||||
|
|
||||||
const files = await loadAttachments(pool, tx, id);
|
const files = await loadAttachments(provider, tx, id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...shapeSummary(row),
|
...shapeSummary(row),
|
||||||
lines: lines.recordset.map(shapeLine),
|
lines: lines.rows.map(shapeLine),
|
||||||
files,
|
files,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,37 @@
|
|||||||
import { DELIVERY_TYPE, LIST_DELIVERIES, LIST_DELIVERY_FILES } from '../../providers/mssql/queries/delivery.js';
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
import request from '../request.js';
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import { shapeSummary } from './helpers.js';
|
import { shapeSummary } from './helpers.js';
|
||||||
import { shapeAttachmentMeta } from './attachments.js';
|
import { shapeAttachmentMeta } from './attachments.js';
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists the deliveries of the user's selected company.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @returns {Promise<Array<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string}>>} The deliveries.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const deliveries = await store.delivery.list(42);
|
|
||||||
* // => [{ id, number, type, entityId, externalNumber, date }]
|
|
||||||
*/
|
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId);
|
||||||
|
const runner = makeRunner(provider);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner(queries.LIST_DELIVERIES, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
type: queries.DELIVERY_TYPE,
|
||||||
.input('type', DELIVERY_TYPE)
|
});
|
||||||
.query(LIST_DELIVERIES);
|
|
||||||
|
|
||||||
const filesResult = await pool
|
const filesResult = await runner(queries.LIST_DELIVERY_FILES, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
type: queries.DELIVERY_TYPE,
|
||||||
.input('type', DELIVERY_TYPE)
|
});
|
||||||
.query(LIST_DELIVERY_FILES);
|
|
||||||
|
|
||||||
const filesByManifest = new Map();
|
const filesByManifest = new Map();
|
||||||
for (const row of filesResult.recordset) {
|
for (const row of filesResult.rows) {
|
||||||
const list = filesByManifest.get(row.manifestId) ?? [];
|
const list = filesByManifest.get(row.manifestId) ?? [];
|
||||||
list.push(shapeAttachmentMeta(row));
|
list.push(shapeAttachmentMeta(row));
|
||||||
filesByManifest.set(row.manifestId, list);
|
filesByManifest.set(row.manifestId, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.recordset.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
...shapeSummary(row),
|
...shapeSummary(row),
|
||||||
files: filesByManifest.get(row.id) ?? [],
|
files: filesByManifest.get(row.id) ?? [],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,42 +1,19 @@
|
|||||||
import {
|
import * as mssqlQueries from '../../providers/mssql/queries/delivery.js';
|
||||||
DELETE_LINES_BY_MANIFEST,
|
import * as sqliteQueries from '../../providers/sqlite3/queries/delivery.js';
|
||||||
DELIVERY_TYPE,
|
|
||||||
GET_DELIVERY_HEADER,
|
|
||||||
GET_NEXT_ATTACHMENT_SORT,
|
|
||||||
CREATE_FILE,
|
|
||||||
CREATE_ATTACHMENT,
|
|
||||||
UPDATE_MANIFEST,
|
|
||||||
} from '../../providers/mssql/queries/delivery.js';
|
|
||||||
import request from '../request.js';
|
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import taxEnsure from '../tax/ensure.js';
|
import taxEnsure from '../tax/ensure.js';
|
||||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||||
import { loadDelivery, shapeSummary } from './helpers.js';
|
import { loadDelivery, shapeSummary } from './helpers.js';
|
||||||
import { insertAttachments } from './attachments.js';
|
import { insertAttachments } from './attachments.js';
|
||||||
import { assertSupplier } from './writeLines.js';
|
import { assertSupplier } from './writeLines.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const ensureTax = taxEnsure(pool);
|
const ensureTax = taxEnsure(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a delivery's header and/or lines.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @param {number} id - The id of the delivery to update.
|
|
||||||
* @param {object} patch - The update patch.
|
|
||||||
* @param {string} [patch.number] - The new delivery number.
|
|
||||||
* @param {string} [patch.date] - The new delivery date.
|
|
||||||
* @param {number} [patch.supplierId] - The new supplier id.
|
|
||||||
* @param {string} [patch.externalNumber] - The new external (supplier) reference number.
|
|
||||||
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [patch.lines] - The new delivery lines.
|
|
||||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The updated delivery.
|
|
||||||
* @throws {Error} If the delivery is not found.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const delivery = await store.delivery.update(42, 5, { number: 'D-2', lines: [...] });
|
|
||||||
* // => { id, number, type, entityId, date, lines: [...] }
|
|
||||||
*/
|
|
||||||
return async (googleAuthId, id, patch = {}) => {
|
return async (googleAuthId, id, patch = {}) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
@@ -63,17 +40,14 @@ export default function update(pool) {
|
|||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
const run = async (runner, tx) => {
|
||||||
await tx.begin();
|
const existing = await runner(queries.GET_DELIVERY_HEADER, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: queries.DELIVERY_TYPE,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const current = existing.rows[0];
|
||||||
const existing = await request(pool, tx)
|
|
||||||
.input('id', id)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.input('type', DELIVERY_TYPE)
|
|
||||||
.query(GET_DELIVERY_HEADER);
|
|
||||||
|
|
||||||
const current = existing.recordset[0];
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Delivery not found');
|
throw new Error('Delivery not found');
|
||||||
}
|
}
|
||||||
@@ -99,7 +73,7 @@ export default function update(pool) {
|
|||||||
let supplierId = summary.supplierId;
|
let supplierId = summary.supplierId;
|
||||||
if (hasSupplierId) {
|
if (hasSupplierId) {
|
||||||
supplierId = await assertSupplier(
|
supplierId = await assertSupplier(
|
||||||
pool,
|
provider,
|
||||||
tx,
|
tx,
|
||||||
companyId,
|
companyId,
|
||||||
patch.supplierId ?? null,
|
patch.supplierId ?? null,
|
||||||
@@ -113,22 +87,22 @@ export default function update(pool) {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await request(pool, tx)
|
await runner(queries.UPDATE_MANIFEST, {
|
||||||
.input('id', id)
|
id,
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.input('type', DELIVERY_TYPE)
|
type: queries.DELIVERY_TYPE,
|
||||||
.input('number', number)
|
number,
|
||||||
.input('entityId', supplierId)
|
entityId: supplierId,
|
||||||
.input('externalNumber', externalNumber)
|
externalNumber,
|
||||||
.input('date', date)
|
date,
|
||||||
.query(UPDATE_MANIFEST);
|
});
|
||||||
|
|
||||||
if (hasLines) {
|
if (hasLines) {
|
||||||
await request(pool, tx)
|
await runner(queries.DELETE_LINES_BY_MANIFEST, {
|
||||||
.input('manifestId', id)
|
manifestId: id,
|
||||||
.query(DELETE_LINES_BY_MANIFEST);
|
});
|
||||||
|
|
||||||
await insertLines(pool, tx, {
|
await insertLines(provider, tx, {
|
||||||
manifestId: id,
|
manifestId: id,
|
||||||
lines: normalizedLines,
|
lines: normalizedLines,
|
||||||
ensureTax,
|
ensureTax,
|
||||||
@@ -137,28 +111,24 @@ export default function update(pool) {
|
|||||||
|
|
||||||
if (hasFiles) {
|
if (hasFiles) {
|
||||||
await insertAttachments(
|
await insertAttachments(
|
||||||
pool,
|
provider,
|
||||||
tx,
|
tx,
|
||||||
{
|
{
|
||||||
GET_NEXT_ATTACHMENT_SORT,
|
GET_NEXT_ATTACHMENT_SORT: queries.GET_NEXT_ATTACHMENT_SORT,
|
||||||
CREATE_FILE,
|
CREATE_FILE: queries.CREATE_FILE,
|
||||||
CREATE_ATTACHMENT,
|
CREATE_ATTACHMENT: queries.CREATE_ATTACHMENT,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
patch.files,
|
patch.files,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const delivery = await loadDelivery(pool, tx, companyId, id);
|
return loadDelivery(provider, tx, companyId, id);
|
||||||
await tx.commit();
|
};
|
||||||
return delivery;
|
|
||||||
} catch (err) {
|
if (typeof provider?.transaction === 'function') {
|
||||||
try {
|
return provider.transaction((runner, tx) => run(runner, tx));
|
||||||
await tx.rollback();
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { GET_SUPPLIER_BY_ID, SUPPLIER_GROUP_NAME } from '../../providers/mssql/queries/entity.js';
|
import * as mssqlQueries from '../../providers/mssql/queries/entity.js';
|
||||||
import request from '../request.js';
|
import * as sqliteQueries from '../../providers/sqlite3/queries/entity.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export async function assertSupplier(pool, tx, companyId, supplierId) {
|
export async function assertSupplier(provider, tx, companyId, supplierId) {
|
||||||
if (supplierId == null) {
|
if (supplierId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -9,13 +10,17 @@ export async function assertSupplier(pool, tx, companyId, supplierId) {
|
|||||||
throw new Error('Supplier not found');
|
throw new Error('Supplier not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
.input('id', supplierId)
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
.input('companyId', companyId)
|
const runner = makeRunner(provider, tx);
|
||||||
.input('groupName', SUPPLIER_GROUP_NAME)
|
|
||||||
.query(GET_SUPPLIER_BY_ID);
|
|
||||||
|
|
||||||
if (!result.recordset[0]) {
|
const result = await runner(queries.GET_SUPPLIER_BY_ID, {
|
||||||
|
id: supplierId,
|
||||||
|
companyId,
|
||||||
|
groupName: queries.SUPPLIER_GROUP_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.rows[0]) {
|
||||||
throw new Error('Supplier not found');
|
throw new Error('Supplier not found');
|
||||||
}
|
}
|
||||||
return supplierId;
|
return supplierId;
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import { CREATE } from '../../providers/mssql/queries/entity.js';
|
import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/entity.js';
|
||||||
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/entityhistory.js';
|
import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/entity.js';
|
||||||
import request from '../request.js';
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/entityhistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/entityhistory.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js';
|
import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js';
|
||||||
import nextNumber from '../numbers/nextNumber.js';
|
import nextNumber from '../numbers/nextNumber.js';
|
||||||
import shape from './shape.js';
|
import shape from './shape.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const groupAccess = resolveCustomerGroupAccess(pool);
|
const groupAccess = resolveCustomerGroupAccess(provider);
|
||||||
const allocateNumber = nextNumber(pool);
|
const allocateNumber = nextNumber(provider);
|
||||||
|
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new entity (customer) in a customer group, recording history.
|
* Creates a new entity (customer) in a customer group, recording history.
|
||||||
* Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding`
|
* Optional `number`; if omitted, allocates via `numbers.nextNumber`
|
||||||
* (`type` `entity`).
|
* (`type` `entity`).
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
@@ -22,6 +29,7 @@ export default function create(pool) {
|
|||||||
* @param {string} input.name - The entity name.
|
* @param {string} input.name - The entity name.
|
||||||
* @param {string} [input.firstName] - The entity first name.
|
* @param {string} [input.firstName] - The entity first name.
|
||||||
* @param {string} [input.number] - Optional explicit customer number.
|
* @param {string} [input.number] - Optional explicit customer number.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The created entity.
|
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The created entity.
|
||||||
* @throws {Error} If the name is empty.
|
* @throws {Error} If the name is empty.
|
||||||
*
|
*
|
||||||
@@ -29,7 +37,7 @@ export default function create(pool) {
|
|||||||
* const entity = await store.entity.create(42, { customergroupId: 5, name: 'Doe', firstName: 'John' });
|
* const entity = await store.entity.create(42, { customergroupId: 5, name: 'Doe', firstName: 'John' });
|
||||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, { customergroupId, name, firstName = null, number } = {}) => {
|
return async (googleAuthId, { customergroupId, name, firstName = null, number } = {}, tx) => {
|
||||||
const cleaned = name?.trim();
|
const cleaned = name?.trim();
|
||||||
if (!cleaned) {
|
if (!cleaned) {
|
||||||
throw new Error('Name required');
|
throw new Error('Name required');
|
||||||
@@ -45,43 +53,50 @@ export default function create(pool) {
|
|||||||
? null
|
? null
|
||||||
: String(number).trim() || null;
|
: String(number).trim() || null;
|
||||||
|
|
||||||
const { companyId, companyuserId } = await access(googleAuthId);
|
const runCreate = async (runner) => {
|
||||||
await groupAccess(companyId, customergroupId);
|
const { companyId, companyuserId } = await access(googleAuthId, runner);
|
||||||
|
await groupAccess(companyId, customergroupId, runner);
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const entityNumber =
|
const entityNumber =
|
||||||
explicitNumber ?? (await allocateNumber(companyId, 'entity', tx));
|
explicitNumber ?? (await allocateNumber(companyId, 'entity', runner));
|
||||||
|
|
||||||
const created = await request(pool, tx)
|
|
||||||
.input('customergroupId', customergroupId)
|
|
||||||
.input('firstName', first)
|
|
||||||
.input('name', cleaned)
|
|
||||||
.input('number', entityNumber)
|
|
||||||
.query(CREATE);
|
|
||||||
|
|
||||||
const row = created.recordset[0];
|
|
||||||
|
|
||||||
await request(pool, tx)
|
|
||||||
.input('entityId', row.id)
|
|
||||||
.input('companyuserId', companyuserId)
|
|
||||||
.input('text', 'Erstellt')
|
|
||||||
.query(INSERT_HISTORY);
|
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
return shape(row);
|
|
||||||
} catch (err) {
|
|
||||||
try {
|
try {
|
||||||
await tx.rollback();
|
const created = await runner.query(createSql, {
|
||||||
} catch {
|
customergroupId,
|
||||||
// ignore
|
firstName: first,
|
||||||
|
name: cleaned,
|
||||||
|
number: entityNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = created.rows[0] ?? {
|
||||||
|
id: created.insertId,
|
||||||
|
customergroupId,
|
||||||
|
firstName: first,
|
||||||
|
name: cleaned,
|
||||||
|
number: entityNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
entityId: row.id,
|
||||||
|
companyuserId,
|
||||||
|
text: 'Erstellt',
|
||||||
|
});
|
||||||
|
|
||||||
|
return shape(row);
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Number already in use');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
if (err.number === 2601 || err.number === 2627) {
|
};
|
||||||
throw new Error('Number already in use');
|
|
||||||
}
|
if (tx) {
|
||||||
throw err;
|
return runCreate(makeRunner(provider, tx));
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runCreate);
|
||||||
|
}
|
||||||
|
return runCreate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,37 @@
|
|||||||
import { DELETE_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/entity.js';
|
import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entity.js';
|
||||||
|
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an entity by its id within the user's selected company.
|
* Deletes an entity by its id within the user's selected company.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The id of the entity to delete.
|
* @param {number} id - The id of the entity to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the entity is not found.
|
* @throws {Error} If the entity is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.entity.delete(42, 5);
|
* await store.entity.delete(42, 5);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(DELETE_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/entity.js';
|
import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/entity.js';
|
||||||
|
import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shape from './shape.js';
|
import shape from './shape.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function getById(pool) {
|
export default function getById(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_ID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a single entity by its id within the user's selected company.
|
* Returns a single entity by its id within the user's selected company.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The id of the entity.
|
* @param {number} id - The id of the entity.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The entity.
|
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The entity.
|
||||||
* @throws {Error} If the entity is not found.
|
* @throws {Error} If the entity is not found.
|
||||||
*
|
*
|
||||||
@@ -17,20 +24,19 @@ export default function getById(pool) {
|
|||||||
* const entity = await store.entity.getById(42, 5);
|
* const entity = await store.entity.getById(42, 5);
|
||||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id) => {
|
return async (googleAuthId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
companyId,
|
||||||
.input('companyId', companyId)
|
});
|
||||||
.query(GET_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { NUMBER_TAKEN_IN_COMPANY } from '../../providers/mssql/queries/entity.js';
|
import { NUMBER_TAKEN_IN_COMPANY as MSSQL_NUMBER_TAKEN } from '../../providers/mssql/queries/entity.js';
|
||||||
|
import { NUMBER_TAKEN_IN_COMPANY as SQLITE_NUMBER_TAKEN } from '../../providers/sqlite3/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function isNumberFree(pool) {
|
export default function isNumberFree(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_NUMBER_TAKEN
|
||||||
|
: MSSQL_NUMBER_TAKEN;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether a customer number is free within the user's selected company.
|
* Returns whether a customer number is free within the user's selected company.
|
||||||
@@ -12,6 +18,7 @@ export default function isNumberFree(pool) {
|
|||||||
* @param {string} number - The customer number to check.
|
* @param {string} number - The customer number to check.
|
||||||
* @param {object} [options] - Optional options.
|
* @param {object} [options] - Optional options.
|
||||||
* @param {number} [options.excludeEntityId] - An entity id to exclude from the check.
|
* @param {number} [options.excludeEntityId] - An entity id to exclude from the check.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<boolean>} Whether the number is free.
|
* @returns {Promise<boolean>} Whether the number is free.
|
||||||
* @throws {Error} If the number is empty.
|
* @throws {Error} If the number is empty.
|
||||||
*
|
*
|
||||||
@@ -21,7 +28,7 @@ export default function isNumberFree(pool) {
|
|||||||
* const freeOnEdit = await store.entity.isNumberFree(42, 'C-100', { excludeEntityId: 5 });
|
* const freeOnEdit = await store.entity.isNumberFree(42, 'C-100', { excludeEntityId: 5 });
|
||||||
* // => true
|
* // => true
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, number, { excludeEntityId } = {}) => {
|
return async (googleAuthId, number, { excludeEntityId } = {}, tx) => {
|
||||||
const cleaned =
|
const cleaned =
|
||||||
number == null || number === '' ? null : String(number).trim() || null;
|
number == null || number === '' ? null : String(number).trim() || null;
|
||||||
if (!cleaned) {
|
if (!cleaned) {
|
||||||
@@ -35,15 +42,14 @@ export default function isNumberFree(pool) {
|
|||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
number: cleaned,
|
||||||
.input('number', cleaned)
|
excludeEntityId: excludeEntityId ?? null,
|
||||||
.input('excludeEntityId', excludeEntityId ?? null)
|
});
|
||||||
.query(NUMBER_TAKEN_IN_COMPANY);
|
|
||||||
|
|
||||||
return !result.recordset[0];
|
return !result.rows[0];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
import {
|
import {
|
||||||
LIST_BY_COMPANY,
|
LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY,
|
||||||
LIST_BY_COMPANY_AND_GROUP,
|
LIST_BY_COMPANY_AND_GROUP as MSSQL_LIST_BY_COMPANY_AND_GROUP,
|
||||||
LIST_CUSTOMERS_BY_COMPANY,
|
LIST_CUSTOMERS_BY_COMPANY as MSSQL_LIST_CUSTOMERS_BY_COMPANY,
|
||||||
POS_GROUP_NAME,
|
POS_GROUP_NAME,
|
||||||
SUPPLIER_GROUP_NAME,
|
SUPPLIER_GROUP_NAME,
|
||||||
} from '../../providers/mssql/queries/entity.js';
|
} from '../../providers/mssql/queries/entity.js';
|
||||||
|
import {
|
||||||
|
LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY,
|
||||||
|
LIST_BY_COMPANY_AND_GROUP as SQLITE_LIST_BY_COMPANY_AND_GROUP,
|
||||||
|
LIST_CUSTOMERS_BY_COMPANY as SQLITE_LIST_CUSTOMERS_BY_COMPANY,
|
||||||
|
} from '../../providers/sqlite3/queries/entity.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js';
|
import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js';
|
||||||
import shape from './shape.js';
|
import shape from './shape.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const groupAccess = resolveCustomerGroupAccess(pool);
|
const groupAccess = resolveCustomerGroupAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
|
const listAllSql = isSqlite
|
||||||
|
? SQLITE_LIST_BY_COMPANY
|
||||||
|
: MSSQL_LIST_BY_COMPANY;
|
||||||
|
const listGroupSql = isSqlite
|
||||||
|
? SQLITE_LIST_BY_COMPANY_AND_GROUP
|
||||||
|
: MSSQL_LIST_BY_COMPANY_AND_GROUP;
|
||||||
|
const listCustomersSql = isSqlite
|
||||||
|
? SQLITE_LIST_CUSTOMERS_BY_COMPANY
|
||||||
|
: MSSQL_LIST_CUSTOMERS_BY_COMPANY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the entities (customers) of the user's selected company, optionally
|
* Lists the entities (customers) of the user's selected company, optionally
|
||||||
@@ -21,6 +38,7 @@ export default function list(pool) {
|
|||||||
* @param {object} [options] - Optional options.
|
* @param {object} [options] - Optional options.
|
||||||
* @param {number} [options.customergroupId] - Filter by customer group id.
|
* @param {number} [options.customergroupId] - Filter by customer group id.
|
||||||
* @param {string} [options.role] - Restrict to `'customer'` role if set.
|
* @param {string} [options.role] - Restrict to `'customer'` role if set.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>>} The entities.
|
* @returns {Promise<Array<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>>} The entities.
|
||||||
* @throws {Error} If the role is invalid.
|
* @throws {Error} If the role is invalid.
|
||||||
*
|
*
|
||||||
@@ -28,38 +46,33 @@ export default function list(pool) {
|
|||||||
* const entities = await store.entity.list(42, { customergroupId: 5 });
|
* const entities = await store.entity.list(42, { customergroupId: 5 });
|
||||||
* // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }]
|
* // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, { customergroupId, role } = {}) => {
|
return async (googleAuthId, { customergroupId, role } = {}, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
|
||||||
if (role != null && role !== 'customer') {
|
if (role != null && role !== 'customer') {
|
||||||
throw new Error('Invalid role');
|
throw new Error('Invalid role');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === 'customer') {
|
if (role === 'customer') {
|
||||||
const result = await pool
|
const result = await runner.query(listCustomersSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
supplierGroupName: SUPPLIER_GROUP_NAME,
|
||||||
.input('supplierGroupName', SUPPLIER_GROUP_NAME)
|
posGroupName: POS_GROUP_NAME,
|
||||||
.input('posGroupName', POS_GROUP_NAME)
|
});
|
||||||
.query(LIST_CUSTOMERS_BY_COMPANY);
|
return result.rows.map(shape);
|
||||||
return result.recordset.map(shape);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customergroupId != null) {
|
if (customergroupId != null) {
|
||||||
await groupAccess(companyId, customergroupId);
|
await groupAccess(companyId, customergroupId, tx);
|
||||||
const result = await pool
|
const result = await runner.query(listGroupSql, {
|
||||||
.request()
|
companyId,
|
||||||
.input('companyId', companyId)
|
customergroupId,
|
||||||
.input('customergroupId', customergroupId)
|
});
|
||||||
.query(LIST_BY_COMPANY_AND_GROUP);
|
return result.rows.map(shape);
|
||||||
return result.recordset.map(shape);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(listAllSql, { companyId });
|
||||||
.request()
|
return result.rows.map(shape);
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(LIST_BY_COMPANY);
|
|
||||||
|
|
||||||
return result.recordset.map(shape);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import peekNumber from '../numbers/peekNumber.js';
|
import peekNumber from '../numbers/peekNumber.js';
|
||||||
|
|
||||||
export default function peekNextNumber(pool) {
|
export default function peekNextNumber(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const peek = peekNumber(pool);
|
const peek = peekNumber(provider);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next customer number that would be allocated for the user's
|
* Returns the next customer number that would be allocated for the user's
|
||||||
* selected company (`numbers` type `entity`) without consuming it.
|
* selected company (`numbers` type `entity`) without consuming it.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<string>} The next entity number.
|
* @returns {Promise<string>} The next entity number.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const number = await store.entity.peekNextNumber(42);
|
* const number = await store.entity.peekNextNumber(42);
|
||||||
* // => '1'
|
* // => '1'
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId) => {
|
return async (googleAuthId, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
return peek(companyId, 'entity');
|
return peek(companyId, 'entity', tx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/entity.js';
|
import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_COMPANY_AND_ID } from '../../providers/mssql/queries/entity.js';
|
||||||
import request from '../request.js';
|
import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/entity.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
/** Ensure entity belongs to company. Throws Entity not found. */
|
/** Ensure entity belongs to company. Throws Entity not found. */
|
||||||
export default function resolveEntityAccess(pool) {
|
export default function resolveEntityAccess(provider) {
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_ID;
|
||||||
|
|
||||||
return async (companyId, entityId, tx) => {
|
return async (companyId, entityId, tx) => {
|
||||||
if (!Number.isFinite(entityId)) {
|
if (!Number.isFinite(entityId)) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await request(pool, tx)
|
const result = await makeRunner(provider, tx).query(sql, {
|
||||||
.input('id', entityId)
|
id: entityId,
|
||||||
.input('companyId', companyId)
|
companyId,
|
||||||
.query(GET_BY_COMPANY_AND_ID);
|
});
|
||||||
|
|
||||||
const row = result.recordset[0];
|
const row = result.rows[0];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../providers/mssql/queries/entity.js';
|
import {
|
||||||
import { INSERT as INSERT_HISTORY } from '../../providers/mssql/queries/entityhistory.js';
|
GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_COMPANY_AND_ID,
|
||||||
import request from '../request.js';
|
UPDATE as MSSQL_UPDATE,
|
||||||
|
} from '../../providers/mssql/queries/entity.js';
|
||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_COMPANY_AND_ID,
|
||||||
|
UPDATE as SQLITE_UPDATE,
|
||||||
|
} from '../../providers/sqlite3/queries/entity.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/entityhistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/entityhistory.js';
|
||||||
|
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shape from './shape.js';
|
import shape from './shape.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_COMPANY_AND_ID
|
||||||
|
: MSSQL_GET_BY_COMPANY_AND_ID;
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
const insertHistorySql = isSqlite
|
||||||
|
? SQLITE_INSERT_HISTORY
|
||||||
|
: MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates name, first name, and/or customer number of an entity, recording history.
|
* Updates name, first name, and/or customer number of an entity, recording history.
|
||||||
@@ -17,6 +34,7 @@ export default function update(pool) {
|
|||||||
* @param {string} [patch.name] - The new entity name.
|
* @param {string} [patch.name] - The new entity name.
|
||||||
* @param {string} [patch.firstName] - The new first name.
|
* @param {string} [patch.firstName] - The new first name.
|
||||||
* @param {string} [patch.number] - The new customer number.
|
* @param {string} [patch.number] - The new customer number.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The updated entity.
|
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The updated entity.
|
||||||
* @throws {Error} If no valid field is provided or the entity is not found.
|
* @throws {Error} If no valid field is provided or the entity is not found.
|
||||||
*
|
*
|
||||||
@@ -24,7 +42,7 @@ export default function update(pool) {
|
|||||||
* const entity = await store.entity.update(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' });
|
* const entity = await store.entity.update(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' });
|
||||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, id, patch = {}) => {
|
return async (googleAuthId, id, patch = {}, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
@@ -36,102 +54,101 @@ export default function update(pool) {
|
|||||||
throw new Error('Name required');
|
throw new Error('Name required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId, companyuserId } = await access(googleAuthId);
|
const runUpdate = async (runner) => {
|
||||||
|
const { companyId, companyuserId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
const existing = await pool
|
const existing = await runner.query(getSql, { id, companyId });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.query(GET_BY_COMPANY_AND_ID);
|
|
||||||
|
|
||||||
const current = existing.recordset[0];
|
const current = existing.rows[0];
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Entity not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = current.name;
|
|
||||||
if (hasName) {
|
|
||||||
name = patch.name?.trim();
|
|
||||||
if (!name) {
|
|
||||||
throw new Error('Name required');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let firstName = current.firstName ?? null;
|
|
||||||
if (hasFirstName) {
|
|
||||||
firstName =
|
|
||||||
patch.firstName == null || patch.firstName === ''
|
|
||||||
? null
|
|
||||||
: String(patch.firstName).trim() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let number = current.number ?? null;
|
|
||||||
if (hasNumber) {
|
|
||||||
number =
|
|
||||||
patch.number == null || patch.number === ''
|
|
||||||
? null
|
|
||||||
: String(patch.number).trim() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const historyLines = [];
|
|
||||||
const oldFirst = current.firstName ?? '';
|
|
||||||
const newFirst = firstName ?? '';
|
|
||||||
if (oldFirst !== newFirst) {
|
|
||||||
historyLines.push(
|
|
||||||
`Vorname geändert von "${oldFirst}" auf "${newFirst}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (current.name !== name) {
|
|
||||||
historyLines.push(
|
|
||||||
`Name geändert von "${current.name}" auf "${name}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const oldNumber = current.number ?? '';
|
|
||||||
const newNumber = number ?? '';
|
|
||||||
if (oldNumber !== newNumber) {
|
|
||||||
historyLines.push(
|
|
||||||
`Nummer geändert von "${oldNumber}" auf "${newNumber}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!historyLines.length) {
|
|
||||||
return shape(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tx = pool.transaction();
|
|
||||||
await tx.begin();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updated = await request(pool, tx)
|
|
||||||
.input('id', id)
|
|
||||||
.input('firstName', firstName)
|
|
||||||
.input('name', name)
|
|
||||||
.input('number', number)
|
|
||||||
.query(UPDATE);
|
|
||||||
|
|
||||||
const row = updated.recordset[0];
|
|
||||||
if (!row) {
|
|
||||||
throw new Error('Entity not found');
|
throw new Error('Entity not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await request(pool, tx)
|
let name = current.name;
|
||||||
.input('entityId', id)
|
if (hasName) {
|
||||||
.input('companyuserId', companyuserId)
|
name = patch.name?.trim();
|
||||||
.input('text', historyLines.join('\n'))
|
if (!name) {
|
||||||
.query(INSERT_HISTORY);
|
throw new Error('Name required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstName = current.firstName ?? null;
|
||||||
|
if (hasFirstName) {
|
||||||
|
firstName =
|
||||||
|
patch.firstName == null || patch.firstName === ''
|
||||||
|
? null
|
||||||
|
: String(patch.firstName).trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let number = current.number ?? null;
|
||||||
|
if (hasNumber) {
|
||||||
|
number =
|
||||||
|
patch.number == null || patch.number === ''
|
||||||
|
? null
|
||||||
|
: String(patch.number).trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyLines = [];
|
||||||
|
const oldFirst = current.firstName ?? '';
|
||||||
|
const newFirst = firstName ?? '';
|
||||||
|
if (oldFirst !== newFirst) {
|
||||||
|
historyLines.push(
|
||||||
|
`Vorname geändert von "${oldFirst}" auf "${newFirst}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (current.name !== name) {
|
||||||
|
historyLines.push(
|
||||||
|
`Name geändert von "${current.name}" auf "${name}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const oldNumber = current.number ?? '';
|
||||||
|
const newNumber = number ?? '';
|
||||||
|
if (oldNumber !== newNumber) {
|
||||||
|
historyLines.push(
|
||||||
|
`Nummer geändert von "${oldNumber}" auf "${newNumber}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!historyLines.length) {
|
||||||
|
return shape(current);
|
||||||
|
}
|
||||||
|
|
||||||
await tx.commit();
|
|
||||||
return shape(row);
|
|
||||||
} catch (err) {
|
|
||||||
try {
|
try {
|
||||||
await tx.rollback();
|
const updated = await runner.query(updateSql, {
|
||||||
} catch {
|
id,
|
||||||
// ignore
|
firstName,
|
||||||
|
name,
|
||||||
|
number,
|
||||||
|
});
|
||||||
|
|
||||||
|
const row = updated.rows[0] ?? {
|
||||||
|
...current,
|
||||||
|
firstName,
|
||||||
|
name,
|
||||||
|
number,
|
||||||
|
};
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
entityId: id,
|
||||||
|
companyuserId,
|
||||||
|
text: historyLines.join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return shape(row);
|
||||||
|
} catch (err) {
|
||||||
|
if (isUniqueConstraintError(err)) {
|
||||||
|
throw new Error('Number already in use');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
if (err.number === 2601 || err.number === 2627) {
|
};
|
||||||
throw new Error('Number already in use');
|
|
||||||
}
|
if (tx) {
|
||||||
throw err;
|
return runUpdate(makeRunner(provider, tx));
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runUpdate);
|
||||||
|
}
|
||||||
|
return runUpdate(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { CREATE, GET_NEXT_SORT } from '../../providers/mssql/queries/entityaddress.js';
|
import {
|
||||||
|
CREATE as MSSQL_CREATE,
|
||||||
|
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/mssql/queries/entityaddress.js';
|
||||||
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/entityaddress.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -24,9 +32,12 @@ function optionalString(value) {
|
|||||||
return s || null;
|
return s || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new address for an entity.
|
* Creates a new address for an entity.
|
||||||
@@ -43,6 +54,7 @@ export default function create(pool) {
|
|||||||
* @param {string} [input.city] - The city.
|
* @param {string} [input.city] - The city.
|
||||||
* @param {string} [input.county] - The county.
|
* @param {string} [input.county] - The county.
|
||||||
* @param {number} [input.sort] - Optional sort order.
|
* @param {number} [input.sort] - Optional sort order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>} The created address.
|
* @returns {Promise<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>} The created address.
|
||||||
* @throws {Error} If the address type is missing.
|
* @throws {Error} If the address type is missing.
|
||||||
*
|
*
|
||||||
@@ -50,7 +62,7 @@ export default function create(pool) {
|
|||||||
* const address = await store.entityaddress.create(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' });
|
* const address = await store.entityaddress.create(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' });
|
||||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, input = {}) => {
|
return async (googleAuthId, entityId, input = {}, tx) => {
|
||||||
const addressType = input.addressType?.trim?.()
|
const addressType = input.addressType?.trim?.()
|
||||||
? input.addressType.trim()
|
? input.addressType.trim()
|
||||||
: null;
|
: null;
|
||||||
@@ -58,34 +70,45 @@ export default function create(pool) {
|
|||||||
throw new Error('Address type required');
|
throw new Error('Address type required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
let sort = input.sort;
|
let sort = input.sort;
|
||||||
if (sort === undefined || sort === null) {
|
if (sort === undefined || sort === null) {
|
||||||
const next = await pool
|
const next = await runner.query(getSortSql, { entityId });
|
||||||
.request()
|
sort = next.rows[0].sort;
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(GET_NEXT_SORT);
|
|
||||||
sort = next.recordset[0].sort;
|
|
||||||
} else if (!Number.isFinite(sort)) {
|
} else if (!Number.isFinite(sort)) {
|
||||||
throw new Error('Invalid sort');
|
throw new Error('Invalid sort');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(createSql, {
|
||||||
.request()
|
entityId,
|
||||||
.input('entityId', entityId)
|
firstName: optionalString(input.firstName),
|
||||||
.input('firstName', optionalString(input.firstName))
|
name: optionalString(input.name),
|
||||||
.input('name', optionalString(input.name))
|
street: optionalString(input.street),
|
||||||
.input('street', optionalString(input.street))
|
streetNumber: optionalString(input.streetNumber),
|
||||||
.input('streetNumber', optionalString(input.streetNumber))
|
zip: optionalString(input.zip),
|
||||||
.input('zip', optionalString(input.zip))
|
city: optionalString(input.city),
|
||||||
.input('city', optionalString(input.city))
|
county: optionalString(input.county),
|
||||||
.input('county', optionalString(input.county))
|
addressType,
|
||||||
.input('addressType', addressType)
|
sort,
|
||||||
.input('sort', sort)
|
});
|
||||||
.query(CREATE);
|
|
||||||
|
|
||||||
return shape(result.recordset[0]);
|
const row = result.rows[0] ?? {
|
||||||
|
id: result.insertId,
|
||||||
|
entityId,
|
||||||
|
firstName: optionalString(input.firstName),
|
||||||
|
name: optionalString(input.name),
|
||||||
|
street: optionalString(input.street),
|
||||||
|
streetNumber: optionalString(input.streetNumber),
|
||||||
|
zip: optionalString(input.zip),
|
||||||
|
city: optionalString(input.city),
|
||||||
|
county: optionalString(input.county),
|
||||||
|
addressType,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
|
||||||
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { DELETE_BY_ENTITY_AND_ID } from '../../providers/mssql/queries/entityaddress.js';
|
import { DELETE_BY_ENTITY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entityaddress.js';
|
||||||
|
import { DELETE_BY_ENTITY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entityaddress.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an address of an entity.
|
* Deletes an address of an entity.
|
||||||
@@ -12,27 +15,28 @@ export default function del(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} entityId - The id of the entity.
|
* @param {number} entityId - The id of the entity.
|
||||||
* @param {number} id - The id of the address to delete.
|
* @param {number} id - The id of the address to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the address is not found.
|
* @throws {Error} If the address is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.entityaddress.delete(42, 5, 9);
|
* await store.entityaddress.delete(42, 5, 9);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, id) => {
|
return async (googleAuthId, entityId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Address not found');
|
throw new Error('Address not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
entityId,
|
||||||
.input('entityId', entityId)
|
});
|
||||||
.query(DELETE_BY_ENTITY_AND_ID);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Address not found');
|
throw new Error('Address not found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { LIST_BY_ENTITY } from '../../providers/mssql/queries/entityaddress.js';
|
import { LIST_BY_ENTITY as MSSQL_LIST_BY_ENTITY } from '../../providers/mssql/queries/entityaddress.js';
|
||||||
|
import { LIST_BY_ENTITY as SQLITE_LIST_BY_ENTITY } from '../../providers/sqlite3/queries/entityaddress.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -18,30 +20,33 @@ function shape(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_LIST_BY_ENTITY
|
||||||
|
: MSSQL_LIST_BY_ENTITY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the addresses of an entity.
|
* Lists the addresses of an entity.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} entityId - The id of the entity.
|
* @param {number} entityId - The id of the entity.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>>} The addresses.
|
* @returns {Promise<Array<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>>} The addresses.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const addresses = await store.entityaddress.list(42, 5);
|
* const addresses = await store.entityaddress.list(42, 5);
|
||||||
* // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }]
|
* // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId) => {
|
return async (googleAuthId, entityId, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, { entityId });
|
||||||
.request()
|
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(LIST_BY_ENTITY);
|
|
||||||
|
|
||||||
return result.recordset.map(shape);
|
return result.rows.map(shape);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../providers/mssql/queries/entityaddress.js';
|
import {
|
||||||
|
GET_BY_ENTITY_AND_ID as MSSQL_GET_BY_ENTITY_AND_ID,
|
||||||
|
UPDATE as MSSQL_UPDATE,
|
||||||
|
} from '../../providers/mssql/queries/entityaddress.js';
|
||||||
|
import {
|
||||||
|
GET_BY_ENTITY_AND_ID as SQLITE_GET_BY_ENTITY_AND_ID,
|
||||||
|
UPDATE as SQLITE_UPDATE,
|
||||||
|
} from '../../providers/sqlite3/queries/entityaddress.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -24,9 +32,14 @@ function optionalString(value) {
|
|||||||
return s || null;
|
return s || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_ENTITY_AND_ID
|
||||||
|
: MSSQL_GET_BY_ENTITY_AND_ID;
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates an address of an entity.
|
* Updates an address of an entity.
|
||||||
@@ -44,6 +57,7 @@ export default function update(pool) {
|
|||||||
* @param {string} [patch.city] - The city.
|
* @param {string} [patch.city] - The city.
|
||||||
* @param {string} [patch.county] - The county.
|
* @param {string} [patch.county] - The county.
|
||||||
* @param {number} [patch.sort] - The sort order.
|
* @param {number} [patch.sort] - The sort order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>} The updated address.
|
* @returns {Promise<{id: number, entityId: number, firstName: string|null, name: string|null, street: string|null, streetNumber: string|null, zip: string|null, city: string|null, county: string|null, addressType: string, sort: number}>} The updated address.
|
||||||
* @throws {Error} If the address is not found.
|
* @throws {Error} If the address is not found.
|
||||||
*
|
*
|
||||||
@@ -51,21 +65,18 @@ export default function update(pool) {
|
|||||||
* const address = await store.entityaddress.update(42, 5, 9, { city: 'Hamburg' });
|
* const address = await store.entityaddress.update(42, 5, 9, { city: 'Hamburg' });
|
||||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, id, patch = {}) => {
|
return async (googleAuthId, entityId, id, patch = {}, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Address not found');
|
throw new Error('Address not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const existing = await pool
|
const existing = await runner.query(getSql, { id, entityId });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(GET_BY_ENTITY_AND_ID);
|
|
||||||
|
|
||||||
const current = existing.recordset[0];
|
const current = existing.rows[0];
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Address not found');
|
throw new Error('Address not found');
|
||||||
}
|
}
|
||||||
@@ -113,25 +124,34 @@ export default function update(pool) {
|
|||||||
sort = patch.sort;
|
sort = patch.sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(updateSql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
entityId,
|
||||||
.input('entityId', entityId)
|
firstName,
|
||||||
.input('firstName', firstName)
|
name,
|
||||||
.input('name', name)
|
street,
|
||||||
.input('street', street)
|
streetNumber,
|
||||||
.input('streetNumber', streetNumber)
|
zip,
|
||||||
.input('zip', zip)
|
city,
|
||||||
.input('city', city)
|
county,
|
||||||
.input('county', county)
|
addressType,
|
||||||
.input('addressType', addressType)
|
sort,
|
||||||
.input('sort', sort)
|
});
|
||||||
.query(UPDATE);
|
|
||||||
|
const row = result.rows[0] ?? {
|
||||||
|
id,
|
||||||
|
entityId,
|
||||||
|
firstName,
|
||||||
|
name,
|
||||||
|
street,
|
||||||
|
streetNumber,
|
||||||
|
zip,
|
||||||
|
city,
|
||||||
|
county,
|
||||||
|
addressType,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
|
||||||
const row = result.recordset[0];
|
|
||||||
if (!row) {
|
|
||||||
throw new Error('Address not found');
|
|
||||||
}
|
|
||||||
return shape(row);
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { CREATE, GET_NEXT_SORT } from '../../providers/mssql/queries/entitycontact.js';
|
import {
|
||||||
|
CREATE as MSSQL_CREATE,
|
||||||
|
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/mssql/queries/entitycontact.js';
|
||||||
|
import {
|
||||||
|
CREATE as SQLITE_CREATE,
|
||||||
|
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
|
||||||
|
} from '../../providers/sqlite3/queries/entitycontact.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -12,9 +20,12 @@ function shape(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function create(pool) {
|
export default function create(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
|
||||||
|
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new contact for an entity.
|
* Creates a new contact for an entity.
|
||||||
@@ -25,6 +36,7 @@ export default function create(pool) {
|
|||||||
* @param {string} input.contactType - The contact type (e.g. `'email'`).
|
* @param {string} input.contactType - The contact type (e.g. `'email'`).
|
||||||
* @param {string} input.contact - The contact value.
|
* @param {string} input.contact - The contact value.
|
||||||
* @param {number} [input.sort] - Optional sort order.
|
* @param {number} [input.sort] - Optional sort order.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The created contact.
|
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The created contact.
|
||||||
* @throws {Error} If the contact type or value is missing.
|
* @throws {Error} If the contact type or value is missing.
|
||||||
*
|
*
|
||||||
@@ -32,7 +44,7 @@ export default function create(pool) {
|
|||||||
* const contact = await store.entitycontact.create(42, 5, { contactType: 'email', contact: 'a@b.c' });
|
* const contact = await store.entitycontact.create(42, 5, { contactType: 'email', contact: 'a@b.c' });
|
||||||
* // => { id, entityId, contactType, contact, sort }
|
* // => { id, entityId, contactType, contact, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, input = {}) => {
|
return async (googleAuthId, entityId, input = {}, tx) => {
|
||||||
const contactType = input.contactType?.trim?.()
|
const contactType = input.contactType?.trim?.()
|
||||||
? input.contactType.trim()
|
? input.contactType.trim()
|
||||||
: null;
|
: null;
|
||||||
@@ -41,28 +53,33 @@ export default function create(pool) {
|
|||||||
throw new Error('Contact required');
|
throw new Error('Contact required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
let sort = input.sort;
|
let sort = input.sort;
|
||||||
if (sort === undefined || sort === null) {
|
if (sort === undefined || sort === null) {
|
||||||
const next = await pool
|
const next = await runner.query(getSortSql, { entityId });
|
||||||
.request()
|
sort = next.rows[0].sort;
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(GET_NEXT_SORT);
|
|
||||||
sort = next.recordset[0].sort;
|
|
||||||
} else if (!Number.isFinite(sort)) {
|
} else if (!Number.isFinite(sort)) {
|
||||||
throw new Error('Invalid sort');
|
throw new Error('Invalid sort');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(createSql, {
|
||||||
.request()
|
entityId,
|
||||||
.input('entityId', entityId)
|
contactType,
|
||||||
.input('contactType', contactType)
|
contact,
|
||||||
.input('contact', contact)
|
sort,
|
||||||
.input('sort', sort)
|
});
|
||||||
.query(CREATE);
|
|
||||||
|
|
||||||
return shape(result.recordset[0]);
|
const row = result.rows[0] ?? {
|
||||||
|
id: result.insertId,
|
||||||
|
entityId,
|
||||||
|
contactType,
|
||||||
|
contact,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
|
||||||
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { DELETE_BY_ENTITY_AND_ID } from '../../providers/mssql/queries/entitycontact.js';
|
import { DELETE_BY_ENTITY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entitycontact.js';
|
||||||
|
import { DELETE_BY_ENTITY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entitycontact.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function del(pool) {
|
export default function del(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a contact of an entity.
|
* Deletes a contact of an entity.
|
||||||
@@ -12,27 +15,28 @@ export default function del(pool) {
|
|||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} entityId - The id of the entity.
|
* @param {number} entityId - The id of the entity.
|
||||||
* @param {number} id - The id of the contact to delete.
|
* @param {number} id - The id of the contact to delete.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the contact is not found.
|
* @throws {Error} If the contact is not found.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.entitycontact.delete(42, 5, 9);
|
* await store.entitycontact.delete(42, 5, 9);
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, id) => {
|
return async (googleAuthId, entityId, id, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Contact not found');
|
throw new Error('Contact not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
entityId,
|
||||||
.input('entityId', entityId)
|
});
|
||||||
.query(DELETE_BY_ENTITY_AND_ID);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
if (!result.rowsAffected) {
|
||||||
throw new Error('Contact not found');
|
throw new Error('Contact not found');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { LIST_BY_ENTITY } from '../../providers/mssql/queries/entitycontact.js';
|
import { LIST_BY_ENTITY as MSSQL_LIST_BY_ENTITY } from '../../providers/mssql/queries/entitycontact.js';
|
||||||
|
import { LIST_BY_ENTITY as SQLITE_LIST_BY_ENTITY } from '../../providers/sqlite3/queries/entitycontact.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -12,30 +14,33 @@ function shape(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function list(pool) {
|
export default function list(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const sql =
|
||||||
|
provider?.dialect === 'sqlite3'
|
||||||
|
? SQLITE_LIST_BY_ENTITY
|
||||||
|
: MSSQL_LIST_BY_ENTITY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the contacts of an entity.
|
* Lists the contacts of an entity.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} entityId - The id of the entity.
|
* @param {number} entityId - The id of the entity.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<Array<{id: number, entityId: number, contactType: string, contact: string, sort: number}>>} The contacts.
|
* @returns {Promise<Array<{id: number, entityId: number, contactType: string, contact: string, sort: number}>>} The contacts.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const contacts = await store.entitycontact.list(42, 5);
|
* const contacts = await store.entitycontact.list(42, 5);
|
||||||
* // => [{ id, entityId, contactType, contact, sort }]
|
* // => [{ id, entityId, contactType, contact, sort }]
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId) => {
|
return async (googleAuthId, entityId, tx) => {
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(sql, { entityId });
|
||||||
.request()
|
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(LIST_BY_ENTITY);
|
|
||||||
|
|
||||||
return result.recordset.map(shape);
|
return result.rows.map(shape);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../providers/mssql/queries/entitycontact.js';
|
import {
|
||||||
|
GET_BY_ENTITY_AND_ID as MSSQL_GET_BY_ENTITY_AND_ID,
|
||||||
|
UPDATE as MSSQL_UPDATE,
|
||||||
|
} from '../../providers/mssql/queries/entitycontact.js';
|
||||||
|
import {
|
||||||
|
GET_BY_ENTITY_AND_ID as SQLITE_GET_BY_ENTITY_AND_ID,
|
||||||
|
UPDATE as SQLITE_UPDATE,
|
||||||
|
} from '../../providers/sqlite3/queries/entitycontact.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
function shape(row) {
|
function shape(row) {
|
||||||
return {
|
return {
|
||||||
@@ -12,9 +20,14 @@ function shape(row) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function update(pool) {
|
export default function update(provider) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const entityAccess = resolveEntityAccess(pool);
|
const entityAccess = resolveEntityAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const getSql = isSqlite
|
||||||
|
? SQLITE_GET_BY_ENTITY_AND_ID
|
||||||
|
: MSSQL_GET_BY_ENTITY_AND_ID;
|
||||||
|
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a contact of an entity.
|
* Updates a contact of an entity.
|
||||||
@@ -23,6 +36,7 @@ export default function update(pool) {
|
|||||||
* @param {number} entityId - The id of the entity.
|
* @param {number} entityId - The id of the entity.
|
||||||
* @param {number} id - The id of the contact to update.
|
* @param {number} id - The id of the contact to update.
|
||||||
* @param {object} patch - The update patch (any subset of contact fields).
|
* @param {object} patch - The update patch (any subset of contact fields).
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The updated contact.
|
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The updated contact.
|
||||||
* @throws {Error} If the contact is not found.
|
* @throws {Error} If the contact is not found.
|
||||||
*
|
*
|
||||||
@@ -30,21 +44,18 @@ export default function update(pool) {
|
|||||||
* const contact = await store.entitycontact.update(42, 5, 9, { contact: 'b@c.d' });
|
* const contact = await store.entitycontact.update(42, 5, 9, { contact: 'b@c.d' });
|
||||||
* // => { id, entityId, contactType, contact, sort }
|
* // => { id, entityId, contactType, contact, sort }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, entityId, id, patch = {}) => {
|
return async (googleAuthId, entityId, id, patch = {}, tx) => {
|
||||||
if (!Number.isFinite(id)) {
|
if (!Number.isFinite(id)) {
|
||||||
throw new Error('Contact not found');
|
throw new Error('Contact not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
const runner = makeRunner(provider, tx);
|
||||||
await entityAccess(companyId, entityId);
|
const { companyId } = await access(googleAuthId, tx);
|
||||||
|
await entityAccess(companyId, entityId, tx);
|
||||||
|
|
||||||
const existing = await pool
|
const existing = await runner.query(getSql, { id, entityId });
|
||||||
.request()
|
|
||||||
.input('id', id)
|
|
||||||
.input('entityId', entityId)
|
|
||||||
.query(GET_BY_ENTITY_AND_ID);
|
|
||||||
|
|
||||||
const current = existing.recordset[0];
|
const current = existing.rows[0];
|
||||||
if (!current) {
|
if (!current) {
|
||||||
throw new Error('Contact not found');
|
throw new Error('Contact not found');
|
||||||
}
|
}
|
||||||
@@ -73,19 +84,22 @@ export default function update(pool) {
|
|||||||
sort = patch.sort;
|
sort = patch.sort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pool
|
const result = await runner.query(updateSql, {
|
||||||
.request()
|
id,
|
||||||
.input('id', id)
|
entityId,
|
||||||
.input('entityId', entityId)
|
contactType,
|
||||||
.input('contactType', contactType)
|
contact,
|
||||||
.input('contact', contact)
|
sort,
|
||||||
.input('sort', sort)
|
});
|
||||||
.query(UPDATE);
|
|
||||||
|
const row = result.rows[0] ?? {
|
||||||
|
id,
|
||||||
|
entityId,
|
||||||
|
contactType,
|
||||||
|
contact,
|
||||||
|
sort,
|
||||||
|
};
|
||||||
|
|
||||||
const row = result.recordset[0];
|
|
||||||
if (!row) {
|
|
||||||
throw new Error('Contact not found');
|
|
||||||
}
|
|
||||||
return shape(row);
|
return shape(row);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { SCHEMA_SCRIPTS } from '../providers/mssql/schema.js';
|
|||||||
|
|
||||||
export default function initialize(provider) {
|
export default function initialize(provider) {
|
||||||
/**
|
/**
|
||||||
* Initializes the database schema by running all schema scripts.
|
* Initializes the database schema by delegating to the provider.
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*
|
*
|
||||||
@@ -10,8 +10,16 @@ export default function initialize(provider) {
|
|||||||
* await store.initialize();
|
* await store.initialize();
|
||||||
*/
|
*/
|
||||||
return async () => {
|
return async () => {
|
||||||
|
if (typeof provider.initialize === 'function') {
|
||||||
|
return provider.initialize();
|
||||||
|
}
|
||||||
|
// Transitional fallback for raw mssql pool
|
||||||
for (const script of SCHEMA_SCRIPTS) {
|
for (const script of SCHEMA_SCRIPTS) {
|
||||||
await provider.query(script);
|
if (typeof provider.query === 'function') {
|
||||||
|
await provider.query(script);
|
||||||
|
} else if (typeof provider.request === 'function') {
|
||||||
|
await provider.request().query(script);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user