This commit is contained in:
seb
2026-10-22 12:30:06 +02:00
parent bb880e3c84
commit 9495ad51cc
27 changed files with 539 additions and 238 deletions

View File

@@ -126,6 +126,8 @@ import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGro
import posterminalGetCustomerGroupList from './store/posterminal/getCustomerGroupList.js';
import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js';
import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js';
import posterminalGetDeletedEntityList from './store/posterminal/getDeletedEntityList.js';
import posterminalRecordTombstone from './store/posterminal/recordTombstone.js';
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
import posterminalGetProductList from './store/posterminal/getProductList.js';
@@ -488,6 +490,8 @@ export default class LibDashStore {
getCustomerGroupList: posterminalGetCustomerGroupList(provider),
getCompositeProductCount: posterminalGetCompositeProductCount(provider),
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
getDeletedEntityList: posterminalGetDeletedEntityList(provider),
recordTombstone: posterminalRecordTombstone(provider),
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
getCategoryList: posterminalGetCategoryList(provider),
getProductList: posterminalGetProductList(provider),

View File

@@ -17,29 +17,6 @@ WHERE id = @articleId
`;
export const CREATE_TOMBSTONE = `
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
SELECT
a.company_id,
a.id AS article_id,
a.sku,
COALESCE(
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC)
) AS name,
COALESCE(
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS taxrate,
COALESCE(
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS net
FROM article a
INNER JOIN company co ON co.id = a.company_id
WHERE a.id = @articleId
`;
export const DELETE_PICS_BY_ARTICLE = `
DELETE p
FROM pic p

View File

@@ -482,3 +482,27 @@ INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.compan
WHERE pr.minmultiplier = 1
GROUP BY pr.article_id, pr.customergroup_id
`;
export const RECORD_TOMBSTONE = `
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
SELECT pt.id, @entityType, @entityId
FROM posterminal pt
WHERE pt.company_id = @companyId AND pt.token IS NOT NULL
`;
export const COUNT_DELETED_ENTITIES = `
SELECT COUNT(*) AS count
FROM posterminaltombstone
WHERE posterminal_id = @posterminalId AND id > @cursor
`;
export const LIST_DELETED_ENTITIES = `
SELECT TOP (@limit)
id,
entity_type AS entityType,
entity_id AS entityId
FROM posterminaltombstone
WHERE posterminal_id = @posterminalId AND id > @cursor
ORDER BY id ASC
`;

View File

@@ -392,27 +392,24 @@ BEGIN
END
`;
export const INIT_ARTICLETOMBSTONE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
export const INIT_POSTERMINALTOMBSTONE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminaltombstone')
BEGIN
CREATE TABLE articletombstone (
CREATE TABLE posterminaltombstone (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
article_id INT NULL,
sku NVARCHAR(100) NULL,
name NVARCHAR(255) NULL,
taxrate DECIMAL(18,4) NULL,
net FLOAT NULL,
deleted_at DATETIME2 NOT NULL CONSTRAINT DF_articletombstone_deleted_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT CK_articletombstone_article_or_sku CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
CONSTRAINT FK_articletombstone_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
posterminal_id INT NOT NULL,
entity_type NVARCHAR(50) NOT NULL,
entity_id INT NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_posterminaltombstone_created_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_posterminaltombstone_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
);
CREATE INDEX IX_articletombstone_company_deleted
ON articletombstone (company_id, deleted_at);
CREATE INDEX IX_posterminaltombstone_posterminal_id
ON posterminaltombstone (posterminal_id, id);
END
`;
export const INIT_ARTICLEEAN = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
BEGIN
@@ -1501,12 +1498,12 @@ export const SCHEMA_SCRIPTS = [
INIT_MODELMESSAGE,
INIT_MODELVFS,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,
INIT_COMPANYHISTORY,
INIT_COMPANYSETTING,
INIT_POSTERMINAL,
INIT_POSTERMINALCATEGORY,
INIT_POSTERMINALTOMBSTONE,
INIT_PRINTER,
INIT_LABELTEMPLATE,
INIT_FILE,

View File

@@ -7,29 +7,6 @@ export const {
GET_CHILDREN_IDS, GET_LIST_IDS,
} = queries;
export const CREATE_TOMBSTONE = `
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
SELECT
a.company_id,
a.id AS article_id,
a.sku,
COALESCE(
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC LIMIT 1)
) AS name,
COALESCE(
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
) AS taxrate,
COALESCE(
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
) AS net
FROM article a
INNER JOIN company co ON co.id = a.company_id
WHERE a.id = @articleId
`;
export const DELETE_PICS_BY_ARTICLE = `
DELETE FROM pic
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)

View File

@@ -9,7 +9,7 @@ export const {
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
TOUCH_LASTSEEN,
TOUCH_LASTSEEN, RECORD_TOMBSTONE, COUNT_DELETED_ENTITIES,
} = queries;
export const SET_ON = `
@@ -263,3 +263,15 @@ AND CAST(strftime('%s', a.updated_at) AS INTEGER) > @cursor
ORDER BY lastChanged ASC, a.id ASC
LIMIT @limit
`;
export const LIST_DELETED_ENTITIES = `
SELECT
id,
entity_type AS entityType,
entity_id AS entityId
FROM posterminaltombstone
WHERE posterminal_id = @posterminalId AND id > @cursor
ORDER BY id ASC
LIMIT @limit
`;

View File

@@ -573,22 +573,6 @@ CREATE TABLE IF NOT EXISTS companyhistory (
);
`;
export const INIT_ARTICLETOMBSTONE = `
CREATE TABLE IF NOT EXISTS articletombstone (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
article_id INTEGER,
sku TEXT COLLATE NOCASE,
name TEXT,
taxrate NUMERIC,
net REAL,
deleted_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_articletombstone_company_deleted
ON articletombstone (company_id, deleted_at);
`;
export const INIT_ENTITYHISTORY = `
CREATE TABLE IF NOT EXISTS entityhistory (
@@ -647,6 +631,19 @@ CREATE TABLE IF NOT EXISTS posterminalcategory (
);
`;
export const INIT_POSTERMINALTOMBSTONE = `
CREATE TABLE IF NOT EXISTS posterminaltombstone (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posterminal_id INTEGER NOT NULL,
entity_type TEXT NOT NULL,
entity_id INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_posterminaltombstone_posterminal_id
ON posterminaltombstone (posterminal_id, id);
`;
export const INIT_PRINTER = `
CREATE TABLE IF NOT EXISTS printer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1117,12 +1114,12 @@ export const SCHEMA_SCRIPTS = [
INIT_MODELMESSAGE,
INIT_MODELVFS,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,
INIT_COMPANYHISTORY,
INIT_COMPANYSETTING,
INIT_POSTERMINAL,
INIT_POSTERMINALCATEGORY,
INIT_POSTERMINALTOMBSTONE,
INIT_PRINTER,
INIT_LABELTEMPLATE,
INIT_FILE,

View File

@@ -3,10 +3,12 @@ import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import recordTombstone from '../posterminal/recordTombstone.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const resolve = resolveArticleBySku(provider);
const tombstone = recordTombstone(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
@@ -37,13 +39,13 @@ export default function del(provider) {
const cascade = Boolean(options.cascade);
const runDelete = async (runner) => {
const { articleId } = await resolve(googleAuthId, sku, runner);
const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const deleteArticleAndLists = async (id) => {
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
await runner.query(queries.CREATE_TOMBSTONE, { articleId: id });
await tombstone({ companyId, entityType: 'article', entityId: id }, runner);
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });

View File

@@ -20,6 +20,9 @@ export default function addImage(provider) {
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC;
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
/**
* Adds an image to a category, processing full, thumb, and 32px-edge small-thumb AVIF variants.
@@ -95,6 +98,8 @@ export default function addImage(provider) {
});
}
await runner.query(touchSql, { categoryId });
return {
id: categoryId,
fullHash,

View File

@@ -5,10 +5,12 @@ import * as sqliteArticleDelete from '../../providers/sqlite3/queries/articleDel
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import resolveCategoryAccess from './resolveCategoryAccess.js';
import recordTombstone from '../posterminal/recordTombstone.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const access = resolveCategoryAccess(provider);
const tombstone = recordTombstone(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const articleDeleteQueries = isSqlite ? sqliteArticleDelete : mssqlArticleDelete;
@@ -32,7 +34,7 @@ export default function del(provider) {
*/
return async ({ googleAuthId, id, recursive = false }, tx) => {
const run = async (runner) => {
const { rootCategoryId } = await access(googleAuthId, id, runner);
const { rootCategoryId, companyId } = await access(googleAuthId, id, runner);
if (id === rootCategoryId) {
throw new Error('Cannot delete root category');
@@ -65,6 +67,7 @@ export default function del(provider) {
try {
await runner.query(sql, { id });
await tombstone({ companyId, entityType: 'category', entityId: id }, runner);
} catch (err) {
if (
err.message?.includes('FOREIGN KEY constraint failed') ||
@@ -146,7 +149,7 @@ export default function del(provider) {
const listIds = await runner.query(articleDeleteQueries.GET_LIST_IDS, { articleId: artId });
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
await runner.query(articleDeleteQueries.CREATE_TOMBSTONE, { articleId: artId });
await tombstone({ companyId, entityType: 'article', entityId: artId }, runner);
await runner.query(articleDeleteQueries.DELETE_PICS_BY_ARTICLE, { articleId: artId });
const deleted = await runner.query(articleDeleteQueries.DELETE_BY_ID, { articleId: artId });
@@ -215,6 +218,7 @@ export default function del(provider) {
const reversedIds = [...subtreeIds].reverse();
for (const catId of reversedIds) {
await runner.query(sql, { id: catId });
await tombstone({ companyId, entityType: 'category', entityId: catId }, runner);
}
};

View File

@@ -6,6 +6,10 @@ import makeRunner from '../runner.js';
export default function deleteImage(provider) {
const access = resolveCategoryAccess(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
const isSqlite = provider?.dialect === 'sqlite3';
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
/**
* Deletes the image of a category.
@@ -30,6 +34,8 @@ export default function deleteImage(provider) {
throw new Error('Image not found');
}
await runner.query(touchSql, { categoryId });
return {
id: categoryId,
thumbW: null,

View File

@@ -7,6 +7,10 @@ import makeRunner from '../runner.js';
export default function rename(provider) {
const access = resolveCategoryAccess(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE : MSSQL_UPDATE;
const isSqlite = provider?.dialect === 'sqlite3';
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
/**
* Renames a category and updates its slug.
@@ -33,7 +37,8 @@ export default function rename(provider) {
const slug = slugify(cleaned);
const result = await makeRunner(provider, tx).query(sql, {
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, {
categoryId: id,
languageId,
name: cleaned,
@@ -44,6 +49,8 @@ export default function rename(provider) {
throw new Error('Category not found');
}
await runner.query(touchSql, { categoryId: id });
return { id, name: cleaned, slug };
};
}

View File

@@ -2,10 +2,12 @@ import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE_BY_COMPANY_AND_ID } from '../.
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE_BY_COMPANY_AND_ID } from '../../providers/sqlite3/queries/customergroup.js';
import { isForeignKeyConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import recordTombstone from '../posterminal/recordTombstone.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const access = resolveSelectedCompanyAccess(provider);
const tombstone = recordTombstone(provider);
const sql =
provider?.dialect === 'sqlite3'
? SQLITE_DELETE_BY_COMPANY_AND_ID
@@ -31,7 +33,8 @@ export default function del(provider) {
const { companyId } = await access(googleAuthId, tx);
try {
const result = await makeRunner(provider, tx).query(sql, {
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, {
id,
companyId,
});
@@ -39,6 +42,8 @@ export default function del(provider) {
if (!result.rowsAffected) {
throw new Error('Customer group not found');
}
await tombstone({ companyId, entityType: 'customergroup', entityId: id }, runner);
} catch (err) {
if (isForeignKeyConstraintError(err)) {
throw new Error('Customer group in use');

View File

@@ -9,10 +9,12 @@ import {
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
} from '../../providers/sqlite3/queries/entity.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import recordTombstone from '../posterminal/recordTombstone.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const access = resolveSelectedCompanyAccess(provider);
const tombstone = recordTombstone(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const checkInUseSql = isSqlite ? SQLITE_CHECK_IN_USE : MSSQL_CHECK_IN_USE;
@@ -63,6 +65,8 @@ export default function del(provider) {
if (!result.rowsAffected) {
throw new Error('Entity not found');
}
await tombstone({ companyId, entityType: 'customer', entityId: numericId }, runner);
};
}

View File

@@ -1,11 +1,24 @@
export default function getDeletedEntityCount() {
import { COUNT_DELETED_ENTITIES as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
import { COUNT_DELETED_ENTITIES as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js';
export default function getDeletedEntityCount(provider) {
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
/**
* Counts deleted entities after the given cursor (currently always 0).
* Counts deleted entities for a POS terminal after the given cursor ID.
*
* @param {object} [input]
* @param {number} [input.posterminalId] - The POS terminal id.
* @param {number} [input.cursor=0] - Unix timestamp cursor.
* @returns {Promise<number>} Always 0.
* @param {object} input
* @param {number} input.posterminalId - The POS terminal id.
* @param {number} [input.cursor=0] - The highest synced tombstone ID.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<number>} Count of deleted entities.
*/
return async () => 0;
return async ({ posterminalId, cursor = 0 } = {}, tx) => {
const result = await makeRunner(provider, tx).query(sql, {
posterminalId: Number(posterminalId),
cursor: Number(cursor) || 0,
});
return Number(result.rows[0]?.count ?? 0);
};
}

View File

@@ -0,0 +1,32 @@
import { LIST_DELETED_ENTITIES as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
import { LIST_DELETED_ENTITIES as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js';
export default function getDeletedEntityList(provider) {
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
/**
* Lists deleted entities for a POS terminal after the given cursor ID.
*
* @param {object} input
* @param {number} input.posterminalId - The POS terminal id.
* @param {number} [input.cursor=0] - The highest synced tombstone ID.
* @param {number} [input.limit=200] - Max items to return.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, entityType: string, entityId: number}>>}
*/
return async ({ posterminalId, cursor = 0, limit = 200 } = {}, tx) => {
const safeLimit = Number.isInteger(limit) && limit > 0 ? limit : 200;
const result = await makeRunner(provider, tx).query(sql, {
posterminalId: Number(posterminalId),
cursor: Number(cursor) || 0,
limit: safeLimit,
});
return result.rows.map((row) => ({
id: Number(row.id),
entityType: String(row.entityType),
entityId: Number(row.entityId),
}));
};
}

View File

@@ -0,0 +1,27 @@
import { RECORD_TOMBSTONE as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js';
import { RECORD_TOMBSTONE as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js';
export default function recordTombstone(provider) {
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL;
/**
* Records a tombstone for all active paired POS terminals of a company.
*
* @param {object} input
* @param {number} input.companyId - The company id.
* @param {string} input.entityType - 'category' | 'article' | 'customer' | 'customergroup'
* @param {number} input.entityId - The deleted entity's id.
* @param {object} [tx] - Optional transaction/runner.
* @returns {Promise<void>}
*/
return async ({ companyId, entityType, entityId }, tx) => {
if (!companyId || !entityType || !entityId) return;
const runner = makeRunner(provider, tx);
await runner.query(sql, {
companyId: Number(companyId),
entityType: String(entityType).toLowerCase().trim(),
entityId: Number(entityId),
});
};
}

View File

@@ -21,6 +21,26 @@ export default function setOff(provider) {
? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS;
const addCategoryTombstoneSql = `
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
VALUES (@posterminalId, 'category', @categoryId)
`;
const addArticleTombstonesSql = `
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
SELECT DISTINCT @posterminalId, 'article', ca.article_id
FROM categoryarticle ca
WHERE ca.category_id = @categoryId
AND NOT EXISTS (
SELECT 1
FROM categoryarticle ca2
INNER JOIN posterminalcategory pc ON pc.category_id = ca2.category_id
WHERE ca2.article_id = ca.article_id
AND pc.posterminal_id = @posterminalId
AND pc.category_id <> @categoryId
)
`;
/**
* Removes a category assignment from a posterminal (turns it off).
*
@@ -49,6 +69,16 @@ export default function setOff(provider) {
throw new Error('Posterminal not found');
}
await runner.query(addCategoryTombstoneSql, {
posterminalId,
categoryId,
});
await runner.query(addArticleTombstonesSql, {
posterminalId,
categoryId,
});
await runner.query(setOffSql, {
posterminalId,
categoryId,

View File

@@ -21,6 +21,21 @@ export default function setOffAll(provider) {
? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS;
const addCategoryTombstonesSql = `
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
SELECT posterminal_id, 'category', category_id
FROM posterminalcategory
WHERE posterminal_id = @posterminalId
`;
const addArticleTombstonesSql = `
INSERT INTO posterminaltombstone (posterminal_id, entity_type, entity_id)
SELECT DISTINCT pc.posterminal_id, 'article', ca.article_id
FROM posterminalcategory pc
INNER JOIN categoryarticle ca ON ca.category_id = pc.category_id
WHERE pc.posterminal_id = @posterminalId
`;
/**
* Removes all category assignments from a posterminal.
*
@@ -49,6 +64,9 @@ export default function setOffAll(provider) {
throw new Error('Posterminal not found');
}
await runner.query(addCategoryTombstonesSql, { posterminalId });
await runner.query(addArticleTombstonesSql, { posterminalId });
await runner.query(setOffAllSql, { posterminalId });
const result = await runner.query(listCatIdsSql, { posterminalId });

View File

@@ -20,6 +20,31 @@ export default function setOn(provider) {
const listCatIdsSql = isSqlite
? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS;
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
const touchArticlesSql = isSqlite
? `UPDATE article
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
)`
: `UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
)`;
const clearTombstonesSql = `
DELETE FROM posterminaltombstone
WHERE posterminal_id = @posterminalId
AND (
(entity_type = 'category' AND entity_id = @categoryId)
OR (entity_type = 'article' AND entity_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
))
)
`;
/**
* Assigns a category to a posterminal (turns it on for that category).
@@ -54,6 +79,14 @@ export default function setOn(provider) {
categoryId,
});
await runner.query(clearTombstonesSql, {
posterminalId,
categoryId,
});
await runner.query(touchSql, { categoryId });
await runner.query(touchArticlesSql, { categoryId });
const result = await runner.query(listCatIdsSql, { posterminalId });
return {

View File

@@ -23,6 +23,35 @@ export default function setOnAll(provider) {
? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS;
const rootSql = isSqlite ? SQLITE_ROOT : MSSQL_ROOT;
const touchAllSql = isSqlite
? `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
)
UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (SELECT id FROM cats)`
: `;WITH 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
)
UPDATE category SET updated_at = SYSUTCDATETIME()
WHERE id IN (SELECT id FROM cats)`;
const touchAllArticlesSql = isSqlite
? `UPDATE article
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE company_id = @companyId`
: `UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE company_id = @companyId`;
const clearTombstonesSql = `
DELETE FROM posterminaltombstone
WHERE posterminal_id = @posterminalId
AND entity_type IN ('category', 'article')
`;
/**
* Assigns all categories of the user's selected company to a posterminal.
@@ -65,6 +94,13 @@ export default function setOnAll(provider) {
rootCategoryId,
});
await runner.query(clearTombstonesSql, {
posterminalId,
});
await runner.query(touchAllSql, { rootCategoryId });
await runner.query(touchAllArticlesSql, { companyId });
const result = await runner.query(listCatIdsSql, { posterminalId });
return {