providerrefactor part1

This commit is contained in:
seb
2026-09-21 03:33:55 +02:00
parent 53a20c4353
commit 4ed6260e91
57 changed files with 1662 additions and 145 deletions

44
TODO.md
View File

@@ -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

6
package-lock.json generated
View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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 = {

View File

@@ -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<import('mssql').ConnectionPool>}
*/
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<T>}
*/
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();
}
}

View File

@@ -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<import('../provider.js').QueryResult>} */
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> | T} fn
* @returns {Promise<T>}
*/
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';
}

View File

@@ -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
`;

View File

@@ -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)
`;

View File

@@ -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
`;

View File

@@ -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;

View File

@@ -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)
`;

View File

@@ -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
`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
`;

View File

@@ -0,0 +1 @@
export { ADD } from '../../mssql/queries/categoryarticle.js';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
`;

View File

@@ -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
)
`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
`;

View File

@@ -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
`;

View File

@@ -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)
`;

View File

@@ -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
`;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
]),
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
];

View File

@@ -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;
};
}

View File

@@ -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];
};
}

View File

@@ -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];
};
}

View File

@@ -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;
};
}

View File

@@ -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 });
};
}

View File

@@ -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');
}
};

View File

@@ -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 });
};
}

View File

@@ -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;
};
}

View File

@@ -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 });
};
}

View File

@@ -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.
*

View File

@@ -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];
};
}

View File

@@ -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;
};
}

49
src/store/runner.js Normal file
View File

@@ -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;
}

View File

@@ -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;
};
}

View File

@@ -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.
*

View File

@@ -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];
};
}

View File

@@ -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;
};
}