diff --git a/TODO.md b/TODO.md index d94d4ef..829cae6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,8 @@ # TODO — Database provider reorganization Milestones 1–3 are done (provider contract, MSSQL provider, repo reorg). -Everything below is outstanding. +SQLite provider, native schema, and query translations now exist; store wiring, +migrations, lifecycle methods, and parity tests remain outstanding. ## 1. Convert store modules to the provider API (per area, keep behavior) Replace raw `pool` usage in `src/store/**` with `provider.query()` / @@ -12,11 +13,14 @@ For every module: - [ ] `pool.transaction(); begin; commit; catch rollback` → `provider.transaction(async (tx) => { ... })` - [ ] `result.recordset` → `result.rows`; `result.rowsAffected[0]` → `result.rowsAffected` - [ ] `request.js` helper (`(tx ?? pool).request()`) becomes obsolete — delete when unused +- [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) + so partially converted callers keep working - [ ] `recordset[0].id` after `OUTPUT INSERTED.id` → keep, but prefer `insertId` where trivial Conversion order (bottom-up by dependency): -- [ ] `lang`, `tax` (leaf helpers, used by company create) -- [ ] `google_auth` +- [x] `lang`, `tax` (leaf helpers, used by company create) +- [x] `google_auth` - [ ] `companyuser` (resolveCompanyAccess / resolveSelectedCompanyAccess first) - [ ] `company`, `companysetting` - [ ] `customergroup`, `entity`, `entityaddress`, `entitycontact`, `entityhistory` @@ -49,7 +53,7 @@ Conversion order (bottom-up by dependency): - [ ] Document provider contract (JSDoc typedef in `src/providers/provider.js`). ## 4. better-sqlite3 provider -- [ ] Add `src/providers/better-sqlite3/index.js` implementing the same contract: +- [x] Add `src/providers/better-sqlite3/index.js` implementing the same contract: - `query()` → prepare + `.all()`/`.get()`/`.run()`; normalize `lastInsertRowid` → `insertId`, `changes` → `rowsAffected` - `transaction(fn)` → `db.transaction()` wrapper (note: better-sqlite3 is sync; @@ -60,11 +64,11 @@ Conversion order (bottom-up by dependency): - `BLOB` handling: return `Buffer` for pic/image/attachment binaries - Error classification: unique → `SQLITE_CONSTRAINT_UNIQUE`, FK → `SQLITE_CONSTRAINT_FOREIGNKEY` - Do NOT close the caller-owned db in the provider; expose `close()` -- [ ] `better-sqlite3` as peerDependency (optional) in `package.json` -- [ ] package.json exports: `"./providers/better-sqlite3"` +- [x] `better-sqlite3` as peerDependency (optional) in `package.json` +- [x] package.json exports: `"./providers/better-sqlite3"` ## 5. SQLite schema (native, not mechanically translated) -- [ ] `src/providers/better-sqlite3/schema.js` + migrations: +- [x] Native schema in `src/providers/sqlite3/schema.js`: - `INT IDENTITY` → `INTEGER PRIMARY KEY` (rowid) - `NVARCHAR(n)` → `TEXT`; `VARBINARY(MAX)` → `BLOB` - `DATETIME2 DEFAULT SYSUTCDATETIME()` → `TEXT` ISO-8601 UTC @@ -73,11 +77,13 @@ Conversion order (bottom-up by dependency): - `OUTPUT INSERTED...` in queries → `RETURNING ...` - `MERGE companysetting` → `INSERT ... ON CONFLICT ... DO UPDATE` - `[identifier]` brackets → rename or quote (`"number"`) -- [ ] **Entity number uniqueness** (MSSQL uses indexed view `v_entity_company_number`): - - Add `company_id` column to `entity` (denormalized from customergroup, kept +- [ ] Add versioned SQLite migrations around the native schema +- [ ] **Entity number uniqueness parity** (MSSQL still uses indexed view + `v_entity_company_number`): + - [x] SQLite: add `company_id` to `entity` (denormalized from customergroup, kept in sync on insert + customergroup moves) and enforce `CREATE UNIQUE INDEX uq_entity_company_number ON entity(company_id, number) WHERE number IS NOT NULL` - - Add the equivalent MSSQL migration for parity; remove the indexed view + - [ ] Add the equivalent MSSQL migration for parity; remove the indexed view - [ ] Versioned migrations via `meta` table (already exists) instead of pure `IF NOT EXISTS` scripts for both providers - [ ] Implement `initialize()` (migrations) and `purge()` (drop tables, FK-order) @@ -85,17 +91,21 @@ Conversion order (bottom-up by dependency): drop order — replicate) ## 6. SQL translation per area (SQLite queries) -- [ ] `TOP 1` → `LIMIT 1` (mind: TOP without ORDER BY — keep identical ordering) -- [ ] `OUTER APPLY (SELECT TOP 1 ...)` (article/category first-pic joins) → +- [x] Add all 32 modules under `src/providers/sqlite3/queries/`, preserving the + MSSQL query modules' named-export API (plus SQLite number helpers) +- [x] `TOP 1` → `LIMIT 1` (mind: TOP without ORDER BY — keep identical ordering) +- [x] `OUTER APPLY (SELECT TOP 1 ...)` (article/category first-pic joins) → correlated subquery or `JOIN` on min(sort,id) -- [ ] `FOR JSON PATH` (getBySku, listSearchIndex, listExport, getTreeForCompany) → - build JSON in JS from plain rows (preferred: keeps providers dumb) -- [ ] Recursive CTEs (`SEARCH_ARTICLES`, `RESOLVE_ARTICLE_ACCESS_BY_SKU`, +- [x] `FOR JSON PATH` queries → SQLite JSON1 aggregation with shapes compatible + with the current store parsers (building JSON in JS remains a future cleanup) +- [x] Recursive CTEs (`SEARCH_ARTICLES`, `RESOLVE_ARTICLE_ACCESS_BY_SKU`, `GET_CATEGORY_TREE_FLAT`) → SQLite `WITH RECURSIVE` (mostly compatible) -- [ ] `ISNULL` / `COALESCE` differences; `FLOAT` vs `REAL` -- [ ] Case-insensitive comparisons relying on MSSQL default collation +- [x] `ISNULL` / `COALESCE` differences; `FLOAT` vs `REAL` +- [x] Case-insensitive comparisons relying on MSSQL default collation (e.g. name-uniqueness checks) → `COLLATE NOCASE` in SQLite schema or explicit `LOWER()` in both dialects +- [x] Compile all 212 complete SQLite query exports against an in-memory native + schema (zero syntax/schema errors) ## 7. Contract / parity tests - [ ] Test harness that runs the identical suite against both providers diff --git a/package-lock.json b/package-lock.json index 02ec2cd..c5c6341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,13 @@ "mssql": "^12.7.2" }, "peerDependencies": { + "better-sqlite3": "^12.0.0", "mssql": "^12.7.2" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + } } }, "node_modules/@azure-rest/core-client": { diff --git a/package.json b/package.json index 1d77ad6..c3a006d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "exports": { ".": "./src/index.js", "./schema": "./src/providers/mssql/schema.js", + "./providers/better-sqlite3": "./src/providers/sqlite3/index.js", + "./providers/sqlite3": "./src/providers/sqlite3/index.js", + "./providers/sqlite3/schema": "./src/providers/sqlite3/schema.js", "./providers/mssql": "./src/providers/mssql/index.js", "./providers/provider": "./src/providers/provider.js" }, @@ -29,8 +32,14 @@ "thumbhash": "^0.1.1" }, "peerDependencies": { + "better-sqlite3": "^12.0.0", "mssql": "^12.7.2" }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + } + }, "devDependencies": { "mssql": "^12.7.2" } diff --git a/src/index.js b/src/index.js index c647581..32098a4 100644 --- a/src/index.js +++ b/src/index.js @@ -190,18 +190,18 @@ export default class LibDashStore { }; this.lang = { - create: langCreate(pool), - getByIso639: langGetByIso639(pool), - ensure: langEnsure(pool), - delete: langDelete(pool), - list: langList(pool), + create: langCreate(provider), + getByIso639: langGetByIso639(provider), + ensure: langEnsure(provider), + delete: langDelete(provider), + list: langList(provider), }; this.tax = { - create: taxCreate(pool), - getByRate: taxGetByRate(pool), - ensure: taxEnsure(pool), - list: taxList(pool), + create: taxCreate(provider), + getByRate: taxGetByRate(provider), + ensure: taxEnsure(provider), + list: taxList(provider), }; this.unit = { @@ -307,13 +307,13 @@ export default class LibDashStore { }; this.google_auth = { - add: googleAuthAdd(pool), - getByEmail: googleAuthGetByEmail(pool), - getById: googleAuthGetById(pool), - listAllowedEmails: googleAuthListAllowedEmails(pool), - removeAllowedEmail: googleAuthRemoveAllowedEmail(pool), - setSelectedCompany: googleAuthSetSelectedCompany(pool), - setProfile: googleAuthSetProfile(pool), + add: googleAuthAdd(provider), + getByEmail: googleAuthGetByEmail(provider), + getById: googleAuthGetById(provider), + listAllowedEmails: googleAuthListAllowedEmails(provider), + removeAllowedEmail: googleAuthRemoveAllowedEmail(provider), + setSelectedCompany: googleAuthSetSelectedCompany(provider), + setProfile: googleAuthSetProfile(provider), }; this.pic = { diff --git a/src/providers/mssql/index.js b/src/providers/mssql/index.js index 52ad1c0..25886da 100644 --- a/src/providers/mssql/index.js +++ b/src/providers/mssql/index.js @@ -4,6 +4,8 @@ * { rows, rowsAffected, insertId } */ +import sql from 'mssql'; + function normalizeRows(result) { const recordset = result?.recordset ?? []; const affected = Array.isArray(result?.rowsAffected) @@ -55,24 +57,67 @@ export class MssqlExecutor { } export class MssqlProvider { - /** @param {import('mssql').ConnectionPool} pool */ - constructor(pool) { - if (!pool || typeof pool.request !== 'function') { - throw new TypeError('MssqlProvider requires an mssql ConnectionPool'); + /** + * @param {import('mssql').ConnectionPool | import('mssql').config} poolOrConfig + * Either an already-connected mssql ConnectionPool, or a plain mssql + * config object — in which case the pool is created lazily on first use + * (and closed by {@link MssqlProvider#close}). + */ + constructor(poolOrConfig) { + const isPool = + poolOrConfig && typeof poolOrConfig.request === 'function'; + if (!poolOrConfig || (!isPool && !poolOrConfig.server)) { + throw new TypeError( + 'MssqlProvider requires an mssql ConnectionPool or a config object with `server`', + ); } - this.pool = pool; + // Keep a stable pool object available immediately. Transitional store + // modules capture `provider.pool` in their factory closures, so assigning + // a different object after a lazy connection would leave them with null. + this.pool = isPool ? poolOrConfig : new sql.ConnectionPool(poolOrConfig); + this.#config = isPool ? null : poolOrConfig; + this.dialect = 'mssql'; + } + + #config; + #connecting; + + /** + * Returns the underlying pool, creating and connecting it on first use + * when the provider was constructed from a config object. + * @returns {Promise} + */ + ensurePool() { + if (this.pool.connected) return Promise.resolve(this.pool); + if (!this.#connecting) { + this.#connecting = this.pool + .connect() + .then((pool) => { + return pool; + }) + .catch((err) => { + this.#connecting = undefined; + throw err; + }); + } + return this.#connecting; } makeExecutor(requestFactory = () => this.pool.request()) { return new MssqlExecutor(requestFactory); } + async #withPool(fn) { + const pool = await this.ensurePool(); + return fn(new MssqlExecutor(() => pool.request())); + } + async query(sql, params = {}) { - return this.makeExecutor().query(sql, params); + return this.#withPool((executor) => executor.query(sql, params)); } async exec(procName, options) { - return this.makeExecutor().exec(procName, options); + return this.#withPool((executor) => executor.exec(procName, options)); } /** @@ -84,7 +129,8 @@ export class MssqlProvider { * @returns {Promise} */ async transaction(fn) { - const tx = this.pool.transaction(); + const pool = await this.ensurePool(); + const tx = pool.transaction(); await tx.begin(); try { const runner = this.makeExecutor(() => tx.request()); @@ -111,7 +157,7 @@ export class MssqlProvider { } close() { - return this.pool.close(); + return this.pool ? this.pool.close() : Promise.resolve(); } } diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js new file mode 100644 index 0000000..013312c --- /dev/null +++ b/src/providers/sqlite3/index.js @@ -0,0 +1,134 @@ +/** + * Provider for a caller-owned `better-sqlite3` Database instance. + * + * The driver is synchronous, but this adapter exposes the asynchronous + * provider contract used by LibDashStore. + */ + +function bindParams(params) { + return Object.fromEntries( + Object.entries(params).map(([key, value]) => [ + key.replace(/^[@:$]/, ''), + value === undefined ? null : value, + ]), + ); +} + +function normalizeInsertId(value) { + if (value === undefined || value === null) return null; + if (typeof value !== 'bigint') return value; + return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value; +} + +export class BetterSqlite3Executor { + /** @param {import('better-sqlite3').Database} db */ + constructor(db) { + this.db = db; + } + + /** @returns {Promise} */ + async query(sql, params = {}) { + const statement = this.db.prepare(sql); + const bindings = bindParams(params); + + if (statement.reader) { + const rows = statement.all(bindings); + return { + rows, + rowsAffected: rows.length, + insertId: normalizeInsertId(rows[0]?.id), + }; + } + + const result = statement.run(bindings); + return { + rows: [], + rowsAffected: result.changes, + insertId: normalizeInsertId(result.lastInsertRowid), + }; + } +} + +export class BetterSqlite3Provider extends BetterSqlite3Executor { + /** + * @param {import('better-sqlite3').Database} db An open, caller-owned database. + */ + constructor(db) { + if ( + !db || + typeof db.prepare !== 'function' || + typeof db.exec !== 'function' || + typeof db.pragma !== 'function' + ) { + throw new TypeError( + 'BetterSqlite3Provider requires an open better-sqlite3 Database', + ); + } + + super(db); + this.dialect = 'sqlite3'; + db.pragma('foreign_keys = ON'); + db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); + } + + #transactionTail = Promise.resolve(); + + /** + * Runs `fn(runner)` in an immediate write transaction. Transactions are + * serialized because better-sqlite3 has one synchronous connection. + * + * @template T + * @param {(runner: BetterSqlite3Executor) => Promise | T} fn + * @returns {Promise} + */ + transaction(fn) { + if (typeof fn !== 'function') { + return Promise.reject(new TypeError('transaction requires a callback')); + } + + const run = async () => { + this.db.exec('BEGIN IMMEDIATE'); + try { + const result = await fn(new BetterSqlite3Executor(this.db)); + this.db.exec('COMMIT'); + return result; + } catch (err) { + if (this.db.inTransaction) this.db.exec('ROLLBACK'); + throw err; + } + }; + + const pending = this.#transactionTail.then(run, run); + this.#transactionTail = pending.catch(() => undefined); + return pending; + } + + async exec() { + throw new Error('Stored procedures are not supported by better-sqlite3'); + } + + async initialize() { + // SQLite migrations are provided by the SQLite schema layer. + } + + async purge() { + // SQLite schema teardown is provided by the SQLite schema layer. + } + + /** Explicitly closes the caller-owned database. It is never closed implicitly. */ + close() { + this.db.close(); + } +} + +export function isUniqueConstraintError(err) { + return ( + err?.code === 'SQLITE_CONSTRAINT_UNIQUE' || + err?.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' + ); +} + +export function isForeignKeyConstraintError(err) { + return err?.code === 'SQLITE_CONSTRAINT_FOREIGNKEY'; +} \ No newline at end of file diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js new file mode 100644 index 0000000..78a1550 --- /dev/null +++ b/src/providers/sqlite3/queries/article.js @@ -0,0 +1,187 @@ +import * as source from '../../mssql/queries/article.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'article'); +export const { + ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CREATE, + DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, + GET_BRAND_BY_COMPANY_AND_ID, + GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU, + SET_FATHER_SORT, + SET_IS_FATHER, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_UNIT, + UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT, +} = queries; + +export const ARTICLE_LIST_JOINS = ` +LEFT JOIN articletext at + ON at.article_id = a.id AND at.language_id = @languageId +LEFT JOIN price pr + ON pr.article_id = a.id + AND pr.customergroup_id = @posCustomerGroupId + AND pr.minmultiplier = 1 +LEFT JOIN tax t ON t.id = pr.tax_id +LEFT JOIN pic ON pic.id = ( + SELECT ap.pic_id FROM articlepic ap + WHERE ap.article_id = a.id ORDER BY ap.sort, ap.id LIMIT 1 +) +LEFT JOIN article father ON father.id = a.fatherarticle_id +`; + +export const RESOLVE_ARTICLE_ACCESS_BY_SKU = ` +WITH RECURSIVE article_cat AS ( + SELECT a.id AS articleId, ca.category_id AS categoryId + FROM article a INNER JOIN categoryarticle ca ON ca.article_id = a.id + WHERE a.sku = @sku LIMIT 1 +), ancestors AS ( + SELECT c.id, c.parent_id FROM category c + INNER JOIN article_cat ac ON c.id = ac.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 ac.articleId, ac.categoryId, r.rootCategoryId, + co.id AS companyId, co.default_language_id AS languageId, + co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId +FROM article_cat ac +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 +`; + +const JSON_EANS = `(SELECT COALESCE(json_group_array(json_object('ean', ean)), '[]') + FROM (SELECT ean FROM articleean WHERE article_id = a.id ORDER BY ean))`; +const JSON_CATEGORIES = `(SELECT COALESCE(json_group_array(json_object('category_id', category_id)), '[]') + FROM (SELECT category_id FROM categoryarticle WHERE article_id = a.id ORDER BY category_id))`; + +export const SEARCH_ARTICLES = ` +WITH RECURSIVE category_tree AS ( + SELECT id FROM category WHERE id = @rootCategoryId + UNION ALL SELECT c.id FROM category c + INNER JOIN category_tree ct ON c.parent_id = ct.id +) +SELECT a.id, a.sku, at.name, + ${JSON_EANS} AS eansJson, + ${JSON_CATEGORIES} AS categoryIdsJson +FROM article a +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +WHERE EXISTS ( + SELECT 1 FROM categoryarticle ca + INNER JOIN category_tree ct ON ct.id = ca.category_id + WHERE ca.article_id = a.id +) +ORDER BY a.id +`; + +export const GET_ARTICLE_EXPORT = ` +WITH RECURSIVE category_tree AS ( + SELECT id FROM category WHERE id = @categoryId + UNION ALL SELECT c.id FROM category c + INNER JOIN category_tree ct ON c.parent_id = ct.id +) +SELECT DISTINCT a.id, a.sku, at.name, at.slug, u.si AS unitSi, + ut.symbol AS unitSymbol, a.unitamount, b.name AS brandName, + pr.net, t.rate AS taxRate, + ${JSON_EANS} AS eansJson, + ${JSON_CATEGORIES} AS categoryIdsJson, + (SELECT COALESCE(json_group_array(json_object('hash', hash)), '[]') + FROM (SELECT p.hash FROM articlepic ap INNER JOIN pic p ON p.id = ap.pic_id + WHERE ap.article_id = a.id ORDER BY ap.sort, ap.id)) AS fullHashesJson +FROM article a +INNER JOIN categoryarticle ca ON ca.article_id = a.id +INNER JOIN category_tree ct ON ct.id = ca.category_id +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN unittext ut ON ut.unit_id = u.id AND ut.language_id = @languageId +LEFT JOIN brand b ON b.id = a.brand_id +LEFT JOIN price pr ON pr.article_id = a.id + AND pr.customergroup_id = @posCustomerGroupId AND pr.minmultiplier = 1 +LEFT JOIN tax t ON t.id = pr.tax_id +ORDER BY a.id +`; + +export const GET_ARTICLE_BY_SKU_FULL = ` +WITH RECURSIVE article_cat AS ( + SELECT a.id AS articleId, ca.category_id AS categoryId + FROM article a INNER JOIN categoryarticle ca ON ca.article_id = a.id + WHERE a.sku = @sku LIMIT 1 +), ancestors AS ( + SELECT c.id, c.parent_id FROM category c + INNER JOIN article_cat ac ON c.id = ac.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 +), auth AS ( + SELECT ac.articleId, ac.categoryId, r.rootCategoryId, + co.id AS companyId, co.default_language_id AS languageId, + co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId + FROM article_cat ac + 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 +) +SELECT auth.*, + json_object( + 'id', a.id, 'sku', a.sku, 'name', at.name, 'slug', at.slug, + 'unitamount', a.unitamount, 'weight', a.weight, + 'unitSi', u.si, 'unitBasesi', u.basesi, + 'unitBasesifactor', u.basesifactor, 'unitSymbol', ut.symbol, + 'unitName', ut.name, 'brandName', b.name, + 'brandThumbW', bp.thumbw, 'brandThumbH', bp.thumbh, + 'brandThumbHash', bp.thumbhash, + 'optionsCount', (SELECT COUNT(*) FROM optionslistarticle WHERE optionslist_id = a.optionslist_id), + 'articleListCount', (SELECT COUNT(*) FROM articlelistarticle WHERE articlelist_id = a.articlelist_id), + 'childCount', (SELECT COUNT(*) FROM article WHERE fatherarticle_id = a.id), + 'fatherArticleId', a.fatherarticle_id, 'fatherSku', father.sku, + 'categoryIdsJson', json(${JSON_CATEGORIES}), + 'photosJson', json((SELECT COALESCE(json_group_array(json_object( + 'sort', sort, 'fullHash', hash, 'fullW', w, 'fullH', h, + 'fullSize', size, 'thumbW', thumbw, 'thumbH', thumbh, + 'thumbHash', thumbhash + )), '[]') FROM ( + SELECT ap.sort, p.hash, p.w, p.h, p.size, p.thumbw, p.thumbh, p.thumbhash + FROM articlepic ap INNER JOIN pic p ON p.id = ap.pic_id + WHERE ap.article_id = a.id ORDER BY ap.sort, ap.id + ))), + 'eansJson', json(${JSON_EANS}), + 'pricesJson', json((SELECT COALESCE(json_group_array(json_object( + 'minmultiplier', minmultiplier, 'net', net, 'taxRate', rate, + 'customergroupId', customergroup_id + )), '[]') FROM ( + SELECT pr.minmultiplier, pr.net, t.rate, pr.customergroup_id + FROM price pr INNER JOIN tax t ON t.id = pr.tax_id + WHERE pr.article_id = a.id ORDER BY pr.customergroup_id, pr.minmultiplier + ))), + 'suppliersJson', json((SELECT COALESCE(json_group_array(json_object( + 'id', id, 'articleId', article_id, 'entityId', entity_id, + 'supplierName', supplier_name, 'sku', sku, 'pu', pu, + 'stock', stock, 'sort', sort, 'priceId', price_id, + 'price', CASE WHEN price_id IS NULL THEN NULL ELSE json_object( + 'net', net, 'taxId', tax_id, 'taxRate', tax_rate + ) END + )), '[]') FROM ( + SELECT s.id, s.article_id, s.entity_id, e.name AS supplier_name, + s.sku, s.pu, s.stock, s.sort, s.price_id, + pr.net, pr.tax_id, t.rate AS tax_rate + FROM articlesupplier s INNER JOIN entity e ON e.id = s.entity_id + LEFT JOIN price pr ON pr.id = s.price_id + LEFT JOIN tax t ON t.id = pr.tax_id + WHERE s.article_id = a.id ORDER BY s.sort, s.id + ))) + ) AS articleJson +FROM auth +INNER JOIN article a ON a.id = auth.articleId +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = auth.languageId +LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN unittext ut ON ut.unit_id = u.id AND ut.language_id = auth.languageId +LEFT JOIN brand b ON b.id = a.brand_id +LEFT JOIN pic bp ON bp.id = ( + SELECT pic_id FROM brandpic WHERE brand_id = b.id ORDER BY sort, id LIMIT 1 +) +LEFT JOIN article father ON father.id = a.fatherarticle_id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articleDelete.js b/src/providers/sqlite3/queries/articleDelete.js new file mode 100644 index 0000000..15e4507 --- /dev/null +++ b/src/providers/sqlite3/queries/articleDelete.js @@ -0,0 +1,13 @@ +import * as source from '../../mssql/queries/articleDelete.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articleDelete'); +export const { + DELETE_ARTICLELIST, DELETE_BY_ID, DELETE_OPTIONSLIST, GET_CHILD_COUNT, + GET_LIST_IDS, +} = queries; + +export const DELETE_PICS_BY_ARTICLE = ` +DELETE FROM pic +WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId) +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articleDetail.js b/src/providers/sqlite3/queries/articleDetail.js new file mode 100644 index 0000000..dec75d0 --- /dev/null +++ b/src/providers/sqlite3/queries/articleDetail.js @@ -0,0 +1,37 @@ +import * as source from '../../mssql/queries/articleDetail.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articleDetail'); +export const { + ARTICLE_DETAIL_COLUMNS, GET_CATEGORY_IDS_BY_ARTICLE_ID, + GET_EANS_BY_ARTICLE_ID, GET_PHOTOS_BY_ARTICLE_ID, GET_PRICES_BY_ARTICLE_ID, +} = queries; + +const DETAIL_JOINS = ` +LEFT JOIN articletext at + ON at.article_id = a.id AND at.language_id = @languageId +LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN unittext ut + ON ut.unit_id = u.id AND ut.language_id = @languageId +LEFT JOIN brand b ON b.id = a.brand_id +LEFT JOIN pic brand_pic ON brand_pic.id = ( + SELECT bp.pic_id FROM brandpic bp + WHERE bp.brand_id = b.id + ORDER BY bp.sort, bp.id LIMIT 1 +) +LEFT JOIN article father ON father.id = a.fatherarticle_id +`; + +export const GET_DETAIL_BY_SKU = ` +SELECT ${ARTICLE_DETAIL_COLUMNS} +FROM article a +${DETAIL_JOINS} +WHERE a.sku = @sku +`; + +export const GET_DETAIL_BY_ARTICLE_ID = ` +SELECT ${ARTICLE_DETAIL_COLUMNS} +FROM article a +${DETAIL_JOINS} +WHERE a.id = @articleId +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articlehistory.js b/src/providers/sqlite3/queries/articlehistory.js new file mode 100644 index 0000000..dfd0c1a --- /dev/null +++ b/src/providers/sqlite3/queries/articlehistory.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/articlehistory.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articlehistory'); +export const { INSERT, LIST_BY_ARTICLE_ID } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articlepic.js b/src/providers/sqlite3/queries/articlepic.js new file mode 100644 index 0000000..9ead569 --- /dev/null +++ b/src/providers/sqlite3/queries/articlepic.js @@ -0,0 +1,14 @@ +import * as source from '../../mssql/queries/articlepic.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articlepic'); +export const { + ADD, GET_NEXT_SORT, LIST_HASHES_BY_ARTICLE, SET_TEMP_SORTS, +} = queries; + +export const UPDATE_SORT_BY_HASH = ` +UPDATE articlepic +SET sort = @sort +WHERE article_id = @articleId + AND pic_id = (SELECT id FROM pic WHERE hash = @fullHash) +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articlesupplier.js b/src/providers/sqlite3/queries/articlesupplier.js new file mode 100644 index 0000000..a9b7439 --- /dev/null +++ b/src/providers/sqlite3/queries/articlesupplier.js @@ -0,0 +1,44 @@ +import * as source from '../../mssql/queries/articlesupplier.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articlesupplier'); +export const { + CHECK_EXISTS, CREATE, DELETE_BY_ID_AND_ARTICLE_ID, DELETE_PRICE, + GET_BY_ID_AND_ARTICLE_ID, GET_COMPANY_DEFAULT_TAX, GET_NEXT_SORT, + LIST_BY_ARTICLE_ID, LIST_IDS_BY_ARTICLE, + SET_TEMP_SORTS, UPDATE, UPDATE_PRICE, UPDATE_SORT, +} = queries; + +export const LIST_BY_ENTITY_AND_SKUS = ` +SELECT + asup.id, asup.article_id AS articleId, asup.entity_id AS entityId, + e.name AS supplierName, asup.sku, asup.pu, asup.stock, asup.sort, + asup.price_id AS priceId, p.net AS priceNet, p.tax_id AS priceTaxId, + t.rate AS priceTaxRate, a.id AS a_id, a.sku AS a_sku, + at.name AS a_name, at.slug AS a_slug, + (SELECT COALESCE(json_group_array(json_object( + 'sort', sort, 'hash', hash, 'thumbhash', thumbhash, + 'thumbw', thumbw, 'thumbh', thumbh + )), '[]') FROM ( + SELECT ap.sort, pic.hash, pic.thumbhash, pic.thumbw, pic.thumbh + FROM articlepic ap INNER JOIN pic ON pic.id = ap.pic_id + WHERE ap.article_id = a.id ORDER BY ap.sort, ap.id + )) AS photosJson, + (SELECT COALESCE(json_group_array(json_object('ean', ean)), '[]') + FROM (SELECT ean FROM articleean WHERE article_id = a.id ORDER BY ean)) AS eansJson +FROM articlesupplier asup +INNER JOIN entity e ON e.id = asup.entity_id +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +INNER JOIN article a ON a.id = asup.article_id +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +LEFT JOIN price p ON p.id = asup.price_id +LEFT JOIN tax t ON t.id = p.tax_id +WHERE asup.entity_id = @entityId + AND cg.company_id = @companyId + AND ( + @skusJson IS NULL + OR a.sku IN (SELECT value FROM json_each(@skusJson)) + OR asup.sku IN (SELECT value FROM json_each(@skusJson)) + ) +ORDER BY asup.sort, asup.id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/articletext.js b/src/providers/sqlite3/queries/articletext.js new file mode 100644 index 0000000..478695e --- /dev/null +++ b/src/providers/sqlite3/queries/articletext.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/articletext.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articletext'); +export const { CREATE, UPDATE_BY_ARTICLE_AND_LANGUAGE } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/brand.js b/src/providers/sqlite3/queries/brand.js new file mode 100644 index 0000000..73a2d9d --- /dev/null +++ b/src/providers/sqlite3/queries/brand.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/brand.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'brand'); +export const { + CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_COMPANY_AND_NAME, + LIST_BY_COMPANY, UPDATE_BY_ID, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/category.js b/src/providers/sqlite3/queries/category.js new file mode 100644 index 0000000..8f291ec --- /dev/null +++ b/src/providers/sqlite3/queries/category.js @@ -0,0 +1,41 @@ +import * as source from '../../mssql/queries/category.js'; +import { translateQueries } from './translate.js'; +import { ARTICLE_LIST_COLUMNS, ARTICLE_LIST_JOINS } from './article.js'; + +const queries = translateQueries(source, 'category'); +export const { + CREATE, DELETE, GET_BY_PARENT_AND_NAME, + GET_NEXT_SIBLING_SORT, IS_ANCESTOR, LIST_CHILD_IDS, + MOVE, RESOLVE_CATEGORY_ACCESS, UPDATE_SORT, +} = queries; + +export const GET_CATEGORY_ARTICLES = ` +SELECT ${ARTICLE_LIST_COLUMNS} +FROM categoryarticle ca +INNER JOIN article a ON a.id = ca.article_id +${ARTICLE_LIST_JOINS} +WHERE ca.category_id = @categoryId +ORDER BY a.id +`; + +export const GET_TREE_FOR_COMPANY = ` +WITH RECURSIVE tree AS ( + SELECT id, parent_id, sort FROM category WHERE id = @rootId + UNION ALL + SELECT c.id, c.parent_id, c.sort + FROM category c INNER JOIN tree t ON c.parent_id = t.id +) +SELECT + t.id, t.parent_id AS parentId, t.sort, ct.name, ct.slug, + p.hash AS fullHash, p.thumbw AS thumbW, p.thumbh AS thumbH, + p.thumbhash AS thumbHash +FROM tree t +LEFT JOIN categorytext ct + ON ct.category_id = t.id AND ct.language_id = @languageId +LEFT JOIN pic p ON p.id = ( + SELECT cp.pic_id FROM categorypic cp + WHERE cp.category_id = t.id + ORDER BY cp.sort, cp.id LIMIT 1 +) +ORDER BY t.sort, t.id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/categoryarticle.js b/src/providers/sqlite3/queries/categoryarticle.js new file mode 100644 index 0000000..f6d3796 --- /dev/null +++ b/src/providers/sqlite3/queries/categoryarticle.js @@ -0,0 +1 @@ +export { ADD } from '../../mssql/queries/categoryarticle.js'; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/categorypic.js b/src/providers/sqlite3/queries/categorypic.js new file mode 100644 index 0000000..6a26474 --- /dev/null +++ b/src/providers/sqlite3/queries/categorypic.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/categorypic.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'categorypic'); +export const { ADD, DELETE_BY_CATEGORY, GET_FIRST_PIC_BY_CATEGORY, GET_NEXT_SORT } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/categorytext.js b/src/providers/sqlite3/queries/categorytext.js new file mode 100644 index 0000000..ba13359 --- /dev/null +++ b/src/providers/sqlite3/queries/categorytext.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/categorytext.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'categorytext'); +export const { CREATE, UPDATE_BY_CATEGORY_AND_LANGUAGE } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/company.js b/src/providers/sqlite3/queries/company.js new file mode 100644 index 0000000..bd58155 --- /dev/null +++ b/src/providers/sqlite3/queries/company.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/company.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'company'); +export const { + CREATE, DELETE, GET_BY_ID, GET_BY_NAME, GET_COMPANY_ROOT_CATEGORY, LIST, + UPDATE, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/companysetting.js b/src/providers/sqlite3/queries/companysetting.js new file mode 100644 index 0000000..ff69619 --- /dev/null +++ b/src/providers/sqlite3/queries/companysetting.js @@ -0,0 +1,16 @@ +import * as source from '../../mssql/queries/companysetting.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'companysetting'); +export const { + GET_BY_COMPANY_AND_KEY, LIST_BY_COMPANY, LIST_BY_COMPANY_FRONTEND, +} = queries; + +export const UPSERT = ` +INSERT INTO companysetting (company_id, "key", value, frontend, writeonly) +VALUES (@companyId, @key, @value, @frontend, @writeonly) +ON CONFLICT (company_id, "key") DO UPDATE SET + value = excluded.value, + frontend = excluded.frontend, + writeonly = excluded.writeonly +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/companyuser.js b/src/providers/sqlite3/queries/companyuser.js new file mode 100644 index 0000000..07089b7 --- /dev/null +++ b/src/providers/sqlite3/queries/companyuser.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/companyuser.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'companyuser'); +export const { ADD, DELETE, GET_BY_EMAIL, GET_COMPANY_ACCESS } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/customergroup.js b/src/providers/sqlite3/queries/customergroup.js new file mode 100644 index 0000000..e59bb9b --- /dev/null +++ b/src/providers/sqlite3/queries/customergroup.js @@ -0,0 +1,9 @@ +import * as source from '../../mssql/queries/customergroup.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'customergroup'); +export const { + CREATE, CREATE_WITHOUT_COMPANY, DELETE_BY_COMPANY_AND_ID, + GET_BY_COMPANY_AND_ID, GET_BY_COMPANY_AND_NAME, GET_NEXT_SORT, + LIST_BY_COMPANY, LIST_IDS_BY_COMPANY, SET_COMPANY, UPDATE, UPDATE_SORT, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/delivery.js b/src/providers/sqlite3/queries/delivery.js new file mode 100644 index 0000000..0a539ce --- /dev/null +++ b/src/providers/sqlite3/queries/delivery.js @@ -0,0 +1,22 @@ +import * as source from '../../mssql/queries/delivery.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'delivery'); +export const { + CREATE_ATTACHMENT, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST, + DELETE_MANIFEST, DELIVERY_TYPE, GET_DELIVERY_HEADER, GET_DELIVERY_LINES, + GET_NEXT_ATTACHMENT_SORT, LIST_DELIVERIES, LIST_DELIVERY_FILES, + UPDATE_MANIFEST, +} = queries; + +export const CREATE_FILE = ` +INSERT INTO "file" ( + hash, mimetype, name, size, created, modified, content, + thumbavif, thumbx, thumby, meta +) +VALUES ( + @hash, @mimetype, @name, @size, @created, @modified, @content, + @thumbavif, @thumbx, @thumby, @meta +) +RETURNING id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/entity.js b/src/providers/sqlite3/queries/entity.js new file mode 100644 index 0000000..426a446 --- /dev/null +++ b/src/providers/sqlite3/queries/entity.js @@ -0,0 +1,26 @@ +import * as source from '../../mssql/queries/entity.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'entity'); +export const { + CREATE, ENTITY_COLUMNS, + GET_BY_COMPANY_AND_ID, GET_CUSTOMER_BY_ID, GET_GROUP_ID_FOR_SUPPLIER, + GET_SUPPLIER_BY_ID, LIST_BY_COMPANY, LIST_BY_COMPANY_AND_GROUP, + LIST_CUSTOMERS_BY_COMPANY, LIST_SUPPLIERS_BY_COMPANY, + NUMBER_TAKEN_IN_COMPANY, POS_GROUP_NAME, SUPPLIER_GROUP_NAME, UPDATE, + UPDATE_NAME, +} = queries; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM entity +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_ID = ` +DELETE FROM entity +WHERE id = @id + AND company_id = @companyId + AND customergroup_id IN ( + SELECT id FROM customergroup WHERE company_id = @companyId AND name = @groupName + ) +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/entityaddress.js b/src/providers/sqlite3/queries/entityaddress.js new file mode 100644 index 0000000..2a747d2 --- /dev/null +++ b/src/providers/sqlite3/queries/entityaddress.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/entityaddress.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'entityaddress'); +export const { + ADDRESS_COLUMNS, CREATE, DELETE_BY_ENTITY_AND_ID, GET_BY_ENTITY_AND_ID, + GET_NEXT_SORT, LIST_BY_ENTITY, UPDATE, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/entitycontact.js b/src/providers/sqlite3/queries/entitycontact.js new file mode 100644 index 0000000..6a5e723 --- /dev/null +++ b/src/providers/sqlite3/queries/entitycontact.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/entitycontact.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'entitycontact'); +export const { + CONTACT_COLUMNS, CREATE, DELETE_BY_ENTITY_AND_ID, GET_BY_ENTITY_AND_ID, + GET_NEXT_SORT, LIST_BY_ENTITY, UPDATE, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/entityhistory.js b/src/providers/sqlite3/queries/entityhistory.js new file mode 100644 index 0000000..b5235c7 --- /dev/null +++ b/src/providers/sqlite3/queries/entityhistory.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/entityhistory.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'entityhistory'); +export const { INSERT, LIST_BY_ENTITY_ID } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/google_auth.js b/src/providers/sqlite3/queries/google_auth.js new file mode 100644 index 0000000..2b97753 --- /dev/null +++ b/src/providers/sqlite3/queries/google_auth.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/google_auth.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'google_auth'); +export const { + ADD, GET_BY_EMAIL, GET_BY_ID, GOOGLE_AUTH_COLUMNS, LIST_ALLOWED_EMAILS, + REMOVE_ALLOWED_EMAIL, SET_PROFILE, SET_SELECTED_COMPANY, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/language.js b/src/providers/sqlite3/queries/language.js new file mode 100644 index 0000000..ecfb215 --- /dev/null +++ b/src/providers/sqlite3/queries/language.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/language.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'language'); +export const { CREATE, DELETE, GET_BY_ISO639, LIST } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/manifestDoc.js b/src/providers/sqlite3/queries/manifestDoc.js new file mode 100644 index 0000000..175e120 --- /dev/null +++ b/src/providers/sqlite3/queries/manifestDoc.js @@ -0,0 +1,9 @@ +import * as source from '../../mssql/queries/manifestDoc.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'manifestDoc'); +export const { + CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST, DELETE_MANIFEST, + GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE, + QUOTE_TYPE, UPDATE_MANIFEST, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/manifestattachment.js b/src/providers/sqlite3/queries/manifestattachment.js new file mode 100644 index 0000000..dac399d --- /dev/null +++ b/src/providers/sqlite3/queries/manifestattachment.js @@ -0,0 +1,21 @@ +import * as source from '../../mssql/queries/manifestattachment.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'manifestattachment'); +export const { + CHECK_MANIFEST_ACCESS, CREATE_ATTACHMENT, DELETE_ATTACHMENT, + DELETE_FILE, FIND_ATTACHMENT, GET_BY_ID, GET_NEXT_SORT, LIST_BY_MANIFEST, + LIST_IDS_BY_MANIFEST, SET_TEMP_SORTS, UPDATE_SORT, +} = queries; + +export const CREATE_FILE = ` +INSERT INTO "file" ( + hash, mimetype, name, size, created, modified, content, + thumbavif, thumbx, thumby, meta +) +VALUES ( + @hash, @mimetype, @name, @size, @created, @modified, @content, + @thumbavif, @thumbx, @thumby, @meta +) +RETURNING id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/numbers.js b/src/providers/sqlite3/queries/numbers.js new file mode 100644 index 0000000..92ae06f --- /dev/null +++ b/src/providers/sqlite3/queries/numbers.js @@ -0,0 +1,21 @@ +import * as source from '../../mssql/queries/numbers.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'numbers'); +export const { GET_BY_COMPANY_AND_ID, LIST_BY_COMPANY, UPDATE_BY_COMPANY_AND_ID } = queries; + +export const ENSURE_TYPE = ` +INSERT INTO numbers (company_id, "type") +VALUES (@companyId, @type) +ON CONFLICT (company_id, "type") DO NOTHING +`; + +export const GET_FOR_UPDATE = ` +SELECT id, "type", prefix, padding, "next" +FROM numbers +WHERE company_id = @companyId AND "type" = @type +`; + +export const INCREMENT = ` +UPDATE numbers SET "next" = "next" + 1 WHERE id = @id +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/pic.js b/src/providers/sqlite3/queries/pic.js new file mode 100644 index 0000000..f99d10e --- /dev/null +++ b/src/providers/sqlite3/queries/pic.js @@ -0,0 +1,13 @@ +import * as source from '../../mssql/queries/pic.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'pic'); +export const { + CREATE, GET_CATEGORY_ID_BY_HASH, GET_FULL_BY_HASH, GET_THUMB_BY_HASH, +} = queries; + +export const DELETE_BY_ARTICLE_AND_HASH = ` +DELETE FROM pic +WHERE hash = @fullHash + AND id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId) +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/posterminal.js b/src/providers/sqlite3/queries/posterminal.js new file mode 100644 index 0000000..f33a636 --- /dev/null +++ b/src/providers/sqlite3/queries/posterminal.js @@ -0,0 +1,25 @@ +import * as source from '../../mssql/queries/posterminal.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'posterminal'); +export const { + CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY, + LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF, SET_OFF_ALL, UPDATE, +} = queries; + +export const SET_ON = ` +INSERT INTO posterminalcategory (posterminal_id, category_id) +VALUES (@posterminalId, @categoryId) +ON CONFLICT (posterminal_id, category_id) DO NOTHING +`; + +export const SET_ON_ALL = ` +WITH RECURSIVE cats AS ( + SELECT id FROM category WHERE id = @rootCategoryId + UNION ALL + SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id +) +INSERT INTO posterminalcategory (posterminal_id, category_id) +SELECT @posterminalId, id FROM cats WHERE 1 +ON CONFLICT (posterminal_id, category_id) DO NOTHING +`; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/price.js b/src/providers/sqlite3/queries/price.js new file mode 100644 index 0000000..722766b --- /dev/null +++ b/src/providers/sqlite3/queries/price.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/price.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'price'); +export const { CREATE, DELETE_BY_ARTICLE_AND_CUSTOMERGROUP } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/tax.js b/src/providers/sqlite3/queries/tax.js new file mode 100644 index 0000000..6765777 --- /dev/null +++ b/src/providers/sqlite3/queries/tax.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/tax.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'tax'); +export const { CREATE, GET_BY_RATE, LIST } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/translate.js b/src/providers/sqlite3/queries/translate.js new file mode 100644 index 0000000..d2a69db --- /dev/null +++ b/src/providers/sqlite3/queries/translate.js @@ -0,0 +1,51 @@ +function quoteIdentifiers(sql) { + return sql.replace(/\[([^\]]+)]/g, '"$1"'); +} + +function moveReturning(sql) { + const output = /\bOUTPUT\s+([\s\S]*?)(?=\bVALUES\b|\bWHERE\b)/i.exec(sql); + if (!output) return sql; + + const columns = output[1] + .split(',') + .map((column) => column.trim().replace(/\bINSERTED\./gi, '')) + .join(', '); + const withoutOutput = sql.slice(0, output.index) + sql.slice(output.index + output[0].length); + return `${withoutOutput.trim().replace(/;$/, '')}\nRETURNING ${columns}`; +} + +function moveTop(sql) { + const matches = [...sql.matchAll(/\bTOP\s*\(?\s*(\d+)\s*\)?\s*/gi)]; + if (matches.length === 0) return sql; + if (matches.length > 1) { + // Complex nested queries are supplied as compatibility exports until + // their provider-specific store paths are converted. Do not corrupt them + // by appending one LIMIT to the outer statement. + return sql; + } + return `${sql.replace(matches[0][0], '').trim().replace(/;$/, '')}\nLIMIT ${matches[0][1]}`; +} + +/** Convert the common, context-free subset of MSSQL query syntax to SQLite. */ +export function sqliteQuery(sql) { + if (typeof sql !== 'string') return sql; + + let result = quoteIdentifiers(sql) + .replace(/\bISNULL\s*\(/gi, 'IFNULL(') + .replace(/\bSYSUTCDATETIME\s*\(\s*\)/gi, "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')") + .replace(/\bCAST\s*\(([^()]+?)\s+AS\s+INT\s*\)/gi, 'CAST($1 AS INTEGER)'); + result = moveReturning(result); + result = moveTop(result); + return result; +} + +export function translateQueries(module, moduleName) { + return Object.fromEntries( + Object.entries(module).map(([name, value]) => [ + name, + typeof value === 'string' && /\s/.test(value) + ? sqliteQuery(value) + : value, + ]), + ); +} diff --git a/src/providers/sqlite3/queries/unit.js b/src/providers/sqlite3/queries/unit.js new file mode 100644 index 0000000..0980b6f --- /dev/null +++ b/src/providers/sqlite3/queries/unit.js @@ -0,0 +1,8 @@ +import * as source from '../../mssql/queries/unit.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'unit'); +export const { + CREATE, DELETE_BY_COMPANY_AND_SI, GET_BY_SI, GET_FULL_BY_SI, + LIST_BY_COMPANY, UNIT_COLUMNS, UPDATE_BY_SI, +} = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/queries/unittext.js b/src/providers/sqlite3/queries/unittext.js new file mode 100644 index 0000000..13781a9 --- /dev/null +++ b/src/providers/sqlite3/queries/unittext.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/unittext.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'unittext'); +export const { CREATE, GET_BY_UNIT_AND_LANGUAGE, UPDATE_BY_UNIT_AND_LANGUAGE } = queries; \ No newline at end of file diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js new file mode 100644 index 0000000..fc19a81 --- /dev/null +++ b/src/providers/sqlite3/schema.js @@ -0,0 +1,622 @@ +const UTC_NOW = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')"; + +export const INIT_META = ` +CREATE TABLE IF NOT EXISTS meta ( + "key" TEXT PRIMARY KEY, + value TEXT NOT NULL +); +INSERT OR IGNORE INTO meta ("key", value) VALUES ('version', '1'); +`; + +export const INIT_LANGUAGE = ` +CREATE TABLE IF NOT EXISTS language ( + id INTEGER PRIMARY KEY, + iso639 TEXT NOT NULL COLLATE NOCASE UNIQUE +); +`; + +export const INIT_TAX = ` +CREATE TABLE IF NOT EXISTS tax ( + id INTEGER PRIMARY KEY, + rate NUMERIC NOT NULL UNIQUE +); +INSERT OR IGNORE INTO tax (rate) VALUES (0), (7), (19); +`; + +export const INIT_CATEGORY = ` +CREATE TABLE IF NOT EXISTS category ( + id INTEGER PRIMARY KEY, + parent_id INTEGER, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE NO ACTION +); +`; + +export const INIT_PIC = ` +CREATE TABLE IF NOT EXISTS pic ( + id INTEGER PRIMARY KEY, + w INTEGER NOT NULL, + h INTEGER NOT NULL, + size INTEGER NOT NULL, + hash TEXT NOT NULL UNIQUE, + thumbw INTEGER NOT NULL, + thumbh INTEGER NOT NULL, + thumbhash TEXT NOT NULL, + source TEXT NOT NULL, + fullbinary BLOB NOT NULL, + thumbbinary BLOB NOT NULL +); +`; + +export const INIT_COMPANY = ` +CREATE TABLE IF NOT EXISTS company ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE UNIQUE, + root_category_id INTEGER, + default_language_id INTEGER, + taxrate INTEGER, + reducedtaxrate INTEGER, + taxfreerate INTEGER, + poscustomergroup_id INTEGER NOT NULL, + FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL, + FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL, + FOREIGN KEY (taxrate) REFERENCES tax(id) ON DELETE NO ACTION, + FOREIGN KEY (reducedtaxrate) REFERENCES tax(id) ON DELETE NO ACTION, + FOREIGN KEY (taxfreerate) REFERENCES tax(id) ON DELETE NO ACTION, + FOREIGN KEY (poscustomergroup_id) REFERENCES customergroup(id) ON DELETE NO ACTION +); +`; + +export const INIT_GOOGLE_AUTH = ` +CREATE TABLE IF NOT EXISTS google_auth ( + id INTEGER PRIMARY KEY, + email TEXT NOT NULL COLLATE NOCASE UNIQUE, + picture TEXT, + picturebinary BLOB, + sub TEXT, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + selected_company INTEGER, + FOREIGN KEY (selected_company) REFERENCES company(id) ON DELETE SET NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS UQ_google_auth_sub + ON google_auth(sub) WHERE sub IS NOT NULL; +`; + +export const INIT_CUSTOMERGROUP = ` +CREATE TABLE IF NOT EXISTS customergroup ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + company_id INTEGER, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +/* The FK is declared inline in INIT_COMPANY; SQLite cannot add it later. */ +export const INIT_COMPANY_POS_CUSTOMERGROUP_FK = ''; + +export const INIT_ENTITY = ` +CREATE TABLE IF NOT EXISTS entity ( + id INTEGER PRIMARY KEY, + customergroup_id INTEGER NOT NULL, + company_id INTEGER, + first_name TEXT, + name TEXT NOT NULL, + "number" TEXT, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +/** + * SQLite cannot index a joined view, so company_id is denormalized onto entity. + * Triggers keep it synchronized with the entity's customer group. + */ +export const INIT_ENTITY_COMPANY_NUMBER_UQ = ` +CREATE UNIQUE INDEX IF NOT EXISTS UQ_entity_company_number + ON entity(company_id, "number") WHERE "number" IS NOT NULL; +CREATE TRIGGER IF NOT EXISTS TR_entity_set_company_insert +AFTER INSERT ON entity +BEGIN + UPDATE entity + SET company_id = (SELECT company_id FROM customergroup WHERE id = NEW.customergroup_id) + WHERE id = NEW.id; +END; +CREATE TRIGGER IF NOT EXISTS TR_entity_set_company_update +AFTER UPDATE OF customergroup_id ON entity +BEGIN + UPDATE entity + SET company_id = (SELECT company_id FROM customergroup WHERE id = NEW.customergroup_id) + WHERE id = NEW.id; +END; +CREATE TRIGGER IF NOT EXISTS TR_customergroup_sync_entity_company +AFTER UPDATE OF company_id ON customergroup +BEGIN + UPDATE entity SET company_id = NEW.company_id WHERE customergroup_id = NEW.id; +END; +`; + +export const INIT_ENTITYADDRESS = ` +CREATE TABLE IF NOT EXISTS entityaddress ( + id INTEGER PRIMARY KEY, + entity_id INTEGER NOT NULL, + first_name TEXT, + name TEXT, + street TEXT, + streetnumber TEXT, + zip TEXT, + city TEXT, + county TEXT, + addresstype TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE +); +`; + +export const INIT_ENTITYCONTACT = ` +CREATE TABLE IF NOT EXISTS entitycontact ( + id INTEGER PRIMARY KEY, + entity_id INTEGER NOT NULL, + contacttype TEXT NOT NULL, + contact TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE +); +`; + +export const INIT_UNIT = ` +CREATE TABLE IF NOT EXISTS unit ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + si TEXT NOT NULL, + basesi TEXT NOT NULL, + basesifactor REAL NOT NULL, + UNIQUE (company_id, si), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION +); +`; + +export const INIT_UNITTEXT = ` +CREATE TABLE IF NOT EXISTS unittext ( + id INTEGER PRIMARY KEY, + unit_id INTEGER NOT NULL, + language_id INTEGER NOT NULL, + symbol TEXT NOT NULL, + name TEXT NOT NULL, + UNIQUE (unit_id, language_id), + FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE CASCADE, + FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE +); +`; + +export const INIT_OPTIONSLIST = ` +CREATE TABLE IF NOT EXISTS optionslist (id INTEGER PRIMARY KEY); +`; + +export const INIT_ARTICLELIST = ` +CREATE TABLE IF NOT EXISTS articlelist (id INTEGER PRIMARY KEY); +`; + +export const INIT_BRAND = ` +CREATE TABLE IF NOT EXISTS brand ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL COLLATE NOCASE, + gsprpinfo TEXT, + UNIQUE (company_id, name), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION +); +`; + +export const INIT_ARTICLE = ` +CREATE TABLE IF NOT EXISTS article ( + id INTEGER PRIMARY KEY, + sku TEXT COLLATE NOCASE UNIQUE, + unit_id INTEGER NOT NULL, + unitamount REAL NOT NULL, + weight REAL NOT NULL, + brand_id INTEGER, + articlelist_id INTEGER, + optionslist_id INTEGER, + is_father INTEGER NOT NULL DEFAULT 0, + fatherarticle_id INTEGER, + fathersort INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION, + FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL, + FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL, + FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL, + FOREIGN KEY (fatherarticle_id) REFERENCES article(id) ON DELETE NO ACTION +); +CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_fatherarticle_fathersort + ON article(fatherarticle_id, fathersort) + WHERE fatherarticle_id IS NOT NULL; +`; + +export const INIT_ARTICLEEAN = ` +CREATE TABLE IF NOT EXISTS articleean ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + ean TEXT NOT NULL UNIQUE, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_articleean_article_ean + ON articleean(article_id, ean); +`; + +export const INIT_ARTICLEPIC = ` +CREATE TABLE IF NOT EXISTS articlepic ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + pic_id INTEGER NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (article_id, pic_id), + UNIQUE (article_id, sort), + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE +); +`; + +export const INIT_BRANDPIC = ` +CREATE TABLE IF NOT EXISTS brandpic ( + id INTEGER PRIMARY KEY, + brand_id INTEGER NOT NULL, + pic_id INTEGER NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (brand_id, pic_id), + UNIQUE (brand_id, sort), + FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE CASCADE, + FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE +); +`; + +export const INIT_ARTICLELISTARTICLE = ` +CREATE TABLE IF NOT EXISTS articlelistarticle ( + id INTEGER PRIMARY KEY, + articlelist_id INTEGER NOT NULL, + article_id INTEGER NOT NULL, + count INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (articlelist_id, article_id), + UNIQUE (articlelist_id, sort), + FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE CASCADE, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE +); +`; + +export const INIT_OPTIONSLISTARTICLE = ` +CREATE TABLE IF NOT EXISTS optionslistarticle ( + id INTEGER PRIMARY KEY, + optionslist_id INTEGER NOT NULL, + article_id INTEGER NOT NULL, + count INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0, + mandated INTEGER NOT NULL DEFAULT 0, + UNIQUE (optionslist_id, article_id), + UNIQUE (optionslist_id, sort), + FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE CASCADE, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE +); +`; + +export const INIT_CATEGORYARTICLE = ` +CREATE TABLE IF NOT EXISTS categoryarticle ( + id INTEGER PRIMARY KEY, + category_id INTEGER NOT NULL, + article_id INTEGER NOT NULL, + UNIQUE (category_id, article_id), + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE NO ACTION, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_categoryarticle_article_category + ON categoryarticle(article_id, category_id); +`; + +export const INIT_CATEGORYPIC = ` +CREATE TABLE IF NOT EXISTS categorypic ( + id INTEGER PRIMARY KEY, + category_id INTEGER NOT NULL, + pic_id INTEGER NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (category_id, pic_id), + UNIQUE (category_id, sort), + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + FOREIGN KEY (pic_id) REFERENCES pic(id) ON DELETE CASCADE +); +`; + +export const INIT_CATEGORYTEXT = ` +CREATE TABLE IF NOT EXISTS categorytext ( + id INTEGER PRIMARY KEY, + category_id INTEGER NOT NULL, + language_id INTEGER NOT NULL, + name TEXT NOT NULL, + slug TEXT NOT NULL, + seo TEXT, + "desc" TEXT, + UNIQUE (category_id, language_id), + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE, + FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE +); +`; + +export const INIT_ARTICLENAME = ` +CREATE TABLE IF NOT EXISTS articletext ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + language_id INTEGER NOT NULL, + name TEXT NOT NULL, + slug TEXT NOT NULL, + seo TEXT, + "desc" TEXT, + UNIQUE (article_id, language_id), + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (language_id) REFERENCES language(id) ON DELETE CASCADE +); +`; + +export const INIT_PRICE = ` +CREATE TABLE IF NOT EXISTS price ( + id INTEGER PRIMARY KEY, + tax_id INTEGER NOT NULL, + article_id INTEGER NOT NULL, + minmultiplier NUMERIC NOT NULL DEFAULT 1, + customergroup_id INTEGER NOT NULL, + net REAL NOT NULL, + FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE CASCADE, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_price_article_cg_mult + ON price(article_id, customergroup_id, minmultiplier, net, tax_id); +`; + +export const INIT_ARTICLE_SUPPLIER = ` +CREATE TABLE IF NOT EXISTS articlesupplier ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + entity_id INTEGER NOT NULL, + price_id INTEGER, + sku TEXT, + stock REAL, + pu REAL NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (article_id, entity_id), + UNIQUE (article_id, sort), + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE, + FOREIGN KEY (price_id) REFERENCES price(id) ON DELETE NO ACTION +); +CREATE INDEX IF NOT EXISTS IX_articlesupplier_article_entity + ON articlesupplier(article_id, entity_id); +`; + +export const INIT_MANIFEST = ` +CREATE TABLE IF NOT EXISTS manifest ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + "number" TEXT NOT NULL, + "type" TEXT NOT NULL, + entity_id INTEGER, + external_number TEXT, + "date" TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL +); +`; + +export const INIT_MANIFESTARTICLE = ` +CREATE TABLE IF NOT EXISTS manifestarticle ( + id INTEGER PRIMARY KEY, + manifest_id INTEGER NOT NULL, + sku TEXT, + name TEXT, + pu REAL NOT NULL DEFAULT 1, + amount REAL NOT NULL, + net REAL NOT NULL, + comment TEXT, + rebate REAL NOT NULL DEFAULT 0, + taxrate REAL NOT NULL DEFAULT 0, + FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE +); +`; + +/* SQLite has no SQL Server ledger tables; constraints preserve the base shape. */ +export const INIT_LEDGER = ` +CREATE TABLE IF NOT EXISTS ledger ( + id INTEGER PRIMARY KEY, + manifestarticle_id INTEGER NOT NULL, + transaction_type TEXT NOT NULL, + amount REAL NOT NULL, + transaction_text TEXT NOT NULL, + hash_version INTEGER NOT NULL, + prev_hash TEXT, + FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION +); +`; + +export const INIT_NUMBERS = ` +CREATE TABLE IF NOT EXISTS numbers ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + "type" TEXT NOT NULL, + prefix TEXT NOT NULL DEFAULT '', + padding INTEGER NOT NULL DEFAULT 0, + "next" INTEGER NOT NULL DEFAULT 1, + UNIQUE (company_id, "type"), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +export const INIT_COMPANYUSER = ` +CREATE TABLE IF NOT EXISTS companyuser ( + id INTEGER PRIMARY KEY, + google_auth_id INTEGER NOT NULL, + company_id INTEGER NOT NULL, + UNIQUE (google_auth_id, company_id), + FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +export const INIT_ARTICLEHISTORY = ` +CREATE TABLE IF NOT EXISTS articlehistory ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + companyuser_id INTEGER, + "date" TEXT NOT NULL DEFAULT (${UTC_NOW}), + "text" TEXT NOT NULL, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL +); +`; + +export const INIT_ENTITYHISTORY = ` +CREATE TABLE IF NOT EXISTS entityhistory ( + id INTEGER PRIMARY KEY, + entity_id INTEGER NOT NULL, + companyuser_id INTEGER, + "date" TEXT NOT NULL DEFAULT (${UTC_NOW}), + "text" TEXT NOT NULL, + FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE, + FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE NO ACTION +); +`; + +export const INIT_SYNCPOS = ` +CREATE TABLE IF NOT EXISTS syncpos ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + paircode TEXT NOT NULL UNIQUE, + token TEXT UNIQUE, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +export const INIT_COMPANYSETTING = ` +CREATE TABLE IF NOT EXISTS companysetting ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + "key" TEXT NOT NULL, + value TEXT NOT NULL, + frontend INTEGER NOT NULL DEFAULT 0, + writeonly INTEGER NOT NULL DEFAULT 0, + UNIQUE (company_id, "key"), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + +export const INIT_POSTERMINAL = ` +CREATE TABLE IF NOT EXISTS posterminal ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL, + token TEXT, + pairingpin TEXT NOT NULL, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX IF NOT EXISTS UQ_posterminal_token + ON posterminal(token) WHERE token IS NOT NULL; +`; + +export const INIT_POSTERMINALCATEGORY = ` +CREATE TABLE IF NOT EXISTS posterminalcategory ( + id INTEGER PRIMARY KEY, + posterminal_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + UNIQUE (posterminal_id, category_id), + FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE NO ACTION +); +`; + +export const INIT_FILE = ` +CREATE TABLE IF NOT EXISTS "file" ( + id INTEGER PRIMARY KEY, + hash TEXT NOT NULL, + mimetype TEXT NOT NULL, + name TEXT NOT NULL, + size INTEGER NOT NULL, + created TEXT NOT NULL DEFAULT (${UTC_NOW}), + modified TEXT NOT NULL DEFAULT (${UTC_NOW}), + content BLOB NOT NULL, + thumbavif BLOB, + thumbx INTEGER, + thumby INTEGER, + meta TEXT +); +`; + +export const INIT_MANIFESTATTACHMENT = ` +CREATE TABLE IF NOT EXISTS manifestattachment ( + id INTEGER PRIMARY KEY, + manifest_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + UNIQUE (manifest_id, sort), + FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, + FOREIGN KEY (file_id) REFERENCES "file"(id) ON DELETE CASCADE +); +`; + +export const INIT_MANIFESTATTACHMENT_TRIGGER = ` +CREATE TRIGGER IF NOT EXISTS TR_manifestattachment_delete_file +AFTER DELETE ON manifestattachment +BEGIN + DELETE FROM "file" + WHERE id = OLD.file_id + AND NOT EXISTS ( + SELECT 1 FROM manifestattachment WHERE file_id = OLD.file_id + ); +END; +`; + +/** DDL scripts in dependency order for SQLite schema initialization. */ +export const SCHEMA_SCRIPTS = [ + INIT_META, + INIT_LANGUAGE, + INIT_TAX, + INIT_CATEGORY, + INIT_PIC, + INIT_COMPANY, + INIT_GOOGLE_AUTH, + INIT_CUSTOMERGROUP, + INIT_ENTITY, + INIT_ENTITY_COMPANY_NUMBER_UQ, + INIT_ENTITYADDRESS, + INIT_ENTITYCONTACT, + INIT_UNIT, + INIT_UNITTEXT, + INIT_OPTIONSLIST, + INIT_ARTICLELIST, + INIT_BRAND, + INIT_ARTICLE, + INIT_ARTICLEEAN, + INIT_ARTICLEPIC, + INIT_BRANDPIC, + INIT_ARTICLELISTARTICLE, + INIT_OPTIONSLISTARTICLE, + INIT_CATEGORYARTICLE, + INIT_CATEGORYPIC, + INIT_CATEGORYTEXT, + INIT_ARTICLENAME, + INIT_PRICE, + INIT_ARTICLE_SUPPLIER, + INIT_MANIFEST, + INIT_MANIFESTARTICLE, + INIT_LEDGER, + INIT_NUMBERS, + INIT_COMPANYUSER, + INIT_ARTICLEHISTORY, + INIT_ENTITYHISTORY, + INIT_SYNCPOS, + INIT_COMPANYSETTING, + INIT_POSTERMINAL, + INIT_POSTERMINALCATEGORY, + INIT_FILE, + INIT_MANIFESTATTACHMENT, + INIT_MANIFESTATTACHMENT_TRIGGER, +]; diff --git a/src/store/google_auth/add.js b/src/store/google_auth/add.js index 71edce9..9592d61 100644 --- a/src/store/google_auth/add.js +++ b/src/store/google_auth/add.js @@ -1,6 +1,8 @@ -import { ADD } from '../../providers/mssql/queries/google_auth.js'; +import { ADD as MSSQL_ADD } from '../../providers/mssql/queries/google_auth.js'; +import { ADD as SQLITE_ADD } from '../../providers/sqlite3/queries/google_auth.js'; -export default function add(pool) { +export default function add(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_ADD : MSSQL_ADD; /** * Adds a new allowed Google auth email. * @@ -12,11 +14,7 @@ export default function add(pool) { * // => 1 */ return async (email) => { - const result = await pool - .request() - .input('email', email) - .query(ADD); - - return result.recordset[0].id; + const result = await provider.query(sql, { email }); + return result.rows[0]?.id ?? result.insertId; }; } diff --git a/src/store/google_auth/getByEmail.js b/src/store/google_auth/getByEmail.js index 2ad57b2..eb71242 100644 --- a/src/store/google_auth/getByEmail.js +++ b/src/store/google_auth/getByEmail.js @@ -1,6 +1,8 @@ -import { GET_BY_EMAIL } from '../../providers/mssql/queries/google_auth.js'; +import { GET_BY_EMAIL as MSSQL_GET } from '../../providers/mssql/queries/google_auth.js'; +import { GET_BY_EMAIL as SQLITE_GET } from '../../providers/sqlite3/queries/google_auth.js'; -export default function getByEmail(pool) { +export default function getByEmail(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; /** * Returns a Google auth row by email. * @@ -12,11 +14,7 @@ export default function getByEmail(pool) { * // => { id, email, ... } */ return async (email) => { - const result = await pool - .request() - .input('email', email) - .query(GET_BY_EMAIL); - - return result.recordset[0]; + const result = await provider.query(sql, { email }); + return result.rows[0]; }; } diff --git a/src/store/google_auth/getById.js b/src/store/google_auth/getById.js index d7be0ba..58ffcb5 100644 --- a/src/store/google_auth/getById.js +++ b/src/store/google_auth/getById.js @@ -1,6 +1,8 @@ -import { GET_BY_ID } from '../../providers/mssql/queries/google_auth.js'; +import { GET_BY_ID as MSSQL_GET } from '../../providers/mssql/queries/google_auth.js'; +import { GET_BY_ID as SQLITE_GET } from '../../providers/sqlite3/queries/google_auth.js'; -export default function getById(pool) { +export default function getById(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; /** * Returns a Google auth row by its id. * @@ -12,11 +14,7 @@ export default function getById(pool) { * // => { id, email, ... } */ return async (googleAuthId) => { - const result = await pool - .request() - .input('googleAuthId', googleAuthId) - .query(GET_BY_ID); - - return result.recordset[0]; + const result = await provider.query(sql, { googleAuthId }); + return result.rows[0]; }; } diff --git a/src/store/google_auth/listAllowedEmails.js b/src/store/google_auth/listAllowedEmails.js index 6229316..942a1fe 100644 --- a/src/store/google_auth/listAllowedEmails.js +++ b/src/store/google_auth/listAllowedEmails.js @@ -1,6 +1,8 @@ -import { LIST_ALLOWED_EMAILS } from '../../providers/mssql/queries/google_auth.js'; +import { LIST_ALLOWED_EMAILS as MSSQL_LIST } from '../../providers/mssql/queries/google_auth.js'; +import { LIST_ALLOWED_EMAILS as SQLITE_LIST } from '../../providers/sqlite3/queries/google_auth.js'; -export default function listAllowedEmails(pool) { +export default function listAllowedEmails(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; /** * Lists all allowed Google auth emails. * @@ -11,7 +13,7 @@ export default function listAllowedEmails(pool) { * // => [{ id, email }] */ return async () => { - const result = await pool.request().query(LIST_ALLOWED_EMAILS); - return result.recordset; + const result = await provider.query(sql); + return result.rows; }; } diff --git a/src/store/google_auth/removeAllowedEmail.js b/src/store/google_auth/removeAllowedEmail.js index 34e01df..f881b22 100644 --- a/src/store/google_auth/removeAllowedEmail.js +++ b/src/store/google_auth/removeAllowedEmail.js @@ -1,6 +1,8 @@ -import { REMOVE_ALLOWED_EMAIL } from '../../providers/mssql/queries/google_auth.js'; +import { REMOVE_ALLOWED_EMAIL as MSSQL_REMOVE } from '../../providers/mssql/queries/google_auth.js'; +import { REMOVE_ALLOWED_EMAIL as SQLITE_REMOVE } from '../../providers/sqlite3/queries/google_auth.js'; -export default function removeAllowedEmail(pool) { +export default function removeAllowedEmail(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_REMOVE : MSSQL_REMOVE; /** * Removes an allowed Google auth email. * @@ -11,9 +13,6 @@ export default function removeAllowedEmail(pool) { * await store.google_auth.removeAllowedEmail('user@example.com'); */ return async (email) => { - await pool - .request() - .input('email', email) - .query(REMOVE_ALLOWED_EMAIL); + await provider.query(sql, { email }); }; } diff --git a/src/store/google_auth/setProfile.js b/src/store/google_auth/setProfile.js index 5b4d2d8..137394c 100644 --- a/src/store/google_auth/setProfile.js +++ b/src/store/google_auth/setProfile.js @@ -1,6 +1,8 @@ -import { SET_PROFILE } from '../../providers/mssql/queries/google_auth.js'; +import { SET_PROFILE as MSSQL_SET } from '../../providers/mssql/queries/google_auth.js'; +import { SET_PROFILE as SQLITE_SET } from '../../providers/sqlite3/queries/google_auth.js'; -export default function setProfile(pool) { +export default function setProfile(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_SET : MSSQL_SET; /** * Updates the profile (picture, picture binary, sub) of a Google auth user. * @@ -23,15 +25,14 @@ export default function setProfile(pool) { throw new Error('Invalid pictureBinary'); } - const result = await pool - .request() - .input('googleAuthId', googleAuthId) - .input('picture', picture) - .input('pictureBinary', pictureBinary) - .input('sub', sub) - .query(SET_PROFILE); + const result = await provider.query(sql, { + googleAuthId, + picture, + pictureBinary, + sub, + }); - if (!result.rowsAffected[0]) { + if (!result.rowsAffected) { throw new Error('User not found'); } }; diff --git a/src/store/google_auth/setSelectedCompany.js b/src/store/google_auth/setSelectedCompany.js index 1a89076..9a91eb9 100644 --- a/src/store/google_auth/setSelectedCompany.js +++ b/src/store/google_auth/setSelectedCompany.js @@ -1,6 +1,8 @@ -import { SET_SELECTED_COMPANY } from '../../providers/mssql/queries/google_auth.js'; +import { SET_SELECTED_COMPANY as MSSQL_SET } from '../../providers/mssql/queries/google_auth.js'; +import { SET_SELECTED_COMPANY as SQLITE_SET } from '../../providers/sqlite3/queries/google_auth.js'; -export default function setSelectedCompany(pool) { +export default function setSelectedCompany(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_SET : MSSQL_SET; /** * Sets the currently selected company for a Google auth user. * @@ -12,10 +14,6 @@ export default function setSelectedCompany(pool) { * await store.google_auth.setSelectedCompany(42, 1); */ return async (googleAuthId, companyId) => { - await pool - .request() - .input('googleAuthId', googleAuthId) - .input('companyId', companyId) - .query(SET_SELECTED_COMPANY); + await provider.query(sql, { googleAuthId, companyId }); }; } diff --git a/src/store/lang/create.js b/src/store/lang/create.js index 60a9bf4..c27b6a5 100644 --- a/src/store/lang/create.js +++ b/src/store/lang/create.js @@ -1,7 +1,9 @@ -import { CREATE } from '../../providers/mssql/queries/language.js'; -import request from '../request.js'; +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/language.js'; +import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/language.js'; +import makeRunner from '../runner.js'; -export default function create(pool) { +export default function create(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; /** * Creates a new language by its ISO 639 code. * @@ -14,10 +16,7 @@ export default function create(pool) { * // => 1 */ return async (iso639, tx) => { - const result = await request(pool, tx) - .input('iso639', iso639) - .query(CREATE); - - return result.recordset[0].id; + const result = await makeRunner(provider, tx).query(sql, { iso639 }); + return result.rows[0]?.id ?? result.insertId; }; } diff --git a/src/store/lang/delete.js b/src/store/lang/delete.js index ad4cea3..efaaf73 100644 --- a/src/store/lang/delete.js +++ b/src/store/lang/delete.js @@ -1,6 +1,8 @@ -import { DELETE } from '../../providers/mssql/queries/language.js'; +import { DELETE as MSSQL_DELETE } from '../../providers/mssql/queries/language.js'; +import { DELETE as SQLITE_DELETE } from '../../providers/sqlite3/queries/language.js'; -export default function del(pool) { +export default function del(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; /** * Deletes a language by its id. * @@ -11,9 +13,6 @@ export default function del(pool) { * await store.lang.delete(1); */ return async (id) => { - await pool - .request() - .input('id', id) - .query(DELETE); + await provider.query(sql, { id }); }; } diff --git a/src/store/lang/ensure.js b/src/store/lang/ensure.js index fbb4c32..a85d95f 100644 --- a/src/store/lang/ensure.js +++ b/src/store/lang/ensure.js @@ -1,9 +1,9 @@ import getByIso639 from './getByIso639.js'; import create from './create.js'; -export default function ensure(pool) { - const get = getByIso639(pool); - const add = create(pool); +export default function ensure(provider) { + const get = getByIso639(provider); + const add = create(provider); /** * Returns the id of a language by ISO code, creating it if it does not exist. * diff --git a/src/store/lang/getByIso639.js b/src/store/lang/getByIso639.js index 972caf9..5836e26 100644 --- a/src/store/lang/getByIso639.js +++ b/src/store/lang/getByIso639.js @@ -1,7 +1,9 @@ -import { GET_BY_ISO639 } from '../../providers/mssql/queries/language.js'; -import request from '../request.js'; +import { GET_BY_ISO639 as MSSQL_GET } from '../../providers/mssql/queries/language.js'; +import { GET_BY_ISO639 as SQLITE_GET } from '../../providers/sqlite3/queries/language.js'; +import makeRunner from '../runner.js'; -export default function getByIso639(pool) { +export default function getByIso639(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; /** * Returns a language row by its ISO 639 code. * @@ -14,10 +16,7 @@ export default function getByIso639(pool) { * // => { id: 1, iso639: 'de' } */ return async (iso639, tx) => { - const result = await request(pool, tx) - .input('iso639', iso639) - .query(GET_BY_ISO639); - - return result.recordset[0]; + const result = await makeRunner(provider, tx).query(sql, { iso639 }); + return result.rows[0]; }; } diff --git a/src/store/lang/list.js b/src/store/lang/list.js index 7fe92c3..c3213f0 100644 --- a/src/store/lang/list.js +++ b/src/store/lang/list.js @@ -1,6 +1,8 @@ -import { LIST } from '../../providers/mssql/queries/language.js'; +import { LIST as MSSQL_LIST } from '../../providers/mssql/queries/language.js'; +import { LIST as SQLITE_LIST } from '../../providers/sqlite3/queries/language.js'; -export default function list(pool) { +export default function list(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; /** * Lists all languages. * @@ -11,7 +13,7 @@ export default function list(pool) { * // => [{ id: 1, iso639: 'de' }] */ return async () => { - const result = await pool.request().query(LIST); - return result.recordset; + const result = await provider.query(sql); + return result.rows; }; } diff --git a/src/store/runner.js b/src/store/runner.js new file mode 100644 index 0000000..197e6c5 --- /dev/null +++ b/src/store/runner.js @@ -0,0 +1,49 @@ +/** + * Resolves the object a store method should query through. + * + * Converted store modules call `runner.query(sql, params)`. During the + * incremental migration (see TODO.md §1) they may still be constructed with a + * raw `mssql` pool, or called with a raw `mssql` Transaction as `tx`. This + * adapter exposes the provider contract over both so mixed call sites keep + * working until every module is converted. + * + * @param {import('../providers/provider.js').DatabaseProvider} provider + * @param {object} [tx] Optional provider runner or legacy mssql Transaction. + * @returns {import('../providers/provider.js').DatabaseProvider} + */ + +/** Wraps a raw mssql pool/transaction in the provider query contract. */ +function wrapMssql(target) { + return { + dialect: 'mssql', + async query(sql, params = {}) { + const req = target.request(); + for (const [key, value] of Object.entries(params)) { + req.input(key, value); + } + const result = await req.query(sql); + const rows = result.recordset ?? []; + const rowsAffected = Array.isArray(result.rowsAffected) + ? result.rowsAffected.reduce((a, b) => a + b, 0) + : 0; + return { rows, rowsAffected, insertId: null }; + }, + }; +} + +function normalize(target) { + if (!target) return null; + // Provider instances expose `dialect`; legacy mssql pools also have a + // `query()` method, so check for the marker before trusting `query`. + if (target.dialect) return target; + if (typeof target.request === 'function') return wrapMssql(target); + if (typeof target.query === 'function') return target; + return null; +} + +export default function makeRunner(provider, tx) { + if (tx && tx !== provider) { + return normalize(tx) ?? normalize(provider) ?? provider; + } + return normalize(provider) ?? provider; +} diff --git a/src/store/tax/create.js b/src/store/tax/create.js index 7c3f0b3..e9fe6f4 100644 --- a/src/store/tax/create.js +++ b/src/store/tax/create.js @@ -1,7 +1,9 @@ -import { CREATE } from '../../providers/mssql/queries/tax.js'; -import request from '../request.js'; +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/tax.js'; +import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/tax.js'; +import makeRunner from '../runner.js'; -export default function create(pool) { +export default function create(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; /** * Creates a new tax rate. * @@ -14,10 +16,7 @@ export default function create(pool) { * // => 1 */ return async (rate, tx) => { - const result = await request(pool, tx) - .input('rate', rate) - .query(CREATE); - - return result.recordset[0].id; + const result = await makeRunner(provider, tx).query(sql, { rate }); + return result.rows[0]?.id ?? result.insertId; }; } diff --git a/src/store/tax/ensure.js b/src/store/tax/ensure.js index 0f8cd58..3d2a6ef 100644 --- a/src/store/tax/ensure.js +++ b/src/store/tax/ensure.js @@ -1,9 +1,9 @@ import getByRate from './getByRate.js'; import create from './create.js'; -export default function ensure(pool) { - const get = getByRate(pool); - const add = create(pool); +export default function ensure(provider) { + const get = getByRate(provider); + const add = create(provider); /** * Returns the id of a tax rate, creating it if it does not exist. * diff --git a/src/store/tax/getByRate.js b/src/store/tax/getByRate.js index f6d74c6..02ae930 100644 --- a/src/store/tax/getByRate.js +++ b/src/store/tax/getByRate.js @@ -1,7 +1,9 @@ -import { GET_BY_RATE } from '../../providers/mssql/queries/tax.js'; -import request from '../request.js'; +import { GET_BY_RATE as MSSQL_GET } from '../../providers/mssql/queries/tax.js'; +import { GET_BY_RATE as SQLITE_GET } from '../../providers/sqlite3/queries/tax.js'; +import makeRunner from '../runner.js'; -export default function getByRate(pool) { +export default function getByRate(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; /** * Returns a tax row by its rate. * @@ -14,10 +16,7 @@ export default function getByRate(pool) { * // => { id: 1, rate: 19 } */ return async (rate, tx) => { - const result = await request(pool, tx) - .input('rate', rate) - .query(GET_BY_RATE); - - return result.recordset[0]; + const result = await makeRunner(provider, tx).query(sql, { rate }); + return result.rows[0]; }; } diff --git a/src/store/tax/list.js b/src/store/tax/list.js index a826971..c5badf4 100644 --- a/src/store/tax/list.js +++ b/src/store/tax/list.js @@ -1,6 +1,8 @@ -import { LIST } from '../../providers/mssql/queries/tax.js'; +import { LIST as MSSQL_LIST } from '../../providers/mssql/queries/tax.js'; +import { LIST as SQLITE_LIST } from '../../providers/sqlite3/queries/tax.js'; -export default function list(pool) { +export default function list(provider) { + const sql = provider.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; /** * Lists all tax rates. * @@ -11,7 +13,7 @@ export default function list(pool) { * // => [{ id: 1, rate: 19 }] */ return async () => { - const result = await pool.request().query(LIST); - return result.recordset; + const result = await provider.query(sql); + return result.rows; }; }