This commit is contained in:
seb
2026-10-03 23:13:36 +02:00
parent 1786879c4e
commit 9a7f345d8b
39 changed files with 4611 additions and 50 deletions

View File

@@ -1,8 +1,8 @@
import { getStore } from './store.js';
export async function createUnderParent({ googleAuthId, parentId, name, track_stock }) {
export async function createUnderParent({ googleAuthId, parentId, name, track_stock, sku }) {
const store = await getStore();
return store.article.createUnderParent({ googleAuthId, parentId, name, track_stock });
return store.article.createUnderParent({ googleAuthId, parentId, name, track_stock, sku });
}
export async function getBySku(googleAuthId, sku) {

View File

@@ -0,0 +1,36 @@
import { getStore } from './store.js';
export async function list(googleAuthId) {
const store = await getStore();
return store.modelconversation.list(googleAuthId);
}
export async function create(googleAuthId, data) {
const store = await getStore();
return store.modelconversation.create(googleAuthId, data);
}
export async function get(googleAuthId, conversationId) {
const store = await getStore();
return store.modelconversation.get(googleAuthId, conversationId);
}
export async function setState(googleAuthId, conversationId, state) {
const store = await getStore();
return store.modelconversation.setState(googleAuthId, conversationId, state);
}
export async function listMessages(googleAuthId, conversationId) {
const store = await getStore();
return store.modelmessage.list(googleAuthId, conversationId);
}
export async function addMessage(googleAuthId, conversationId, data) {
const store = await getStore();
return store.modelmessage.add(googleAuthId, conversationId, data);
}
export async function remove(googleAuthId, conversationId) {
const store = await getStore();
return store.modelconversation.delete(googleAuthId, conversationId);
}

26
src/db/modelvfs.js Normal file
View File

@@ -0,0 +1,26 @@
import { getStore } from './store.js';
export async function write(googleAuthId, input) {
const store = await getStore();
return store.modelvfs.write(googleAuthId, input);
}
export async function read(googleAuthId, name) {
const store = await getStore();
return store.modelvfs.read(googleAuthId, name);
}
export async function list(googleAuthId) {
const store = await getStore();
return store.modelvfs.list(googleAuthId);
}
export async function remove(googleAuthId, name) {
const store = await getStore();
return store.modelvfs.delete(googleAuthId, name);
}
export async function reset(googleAuthId) {
const store = await getStore();
return store.modelvfs.reset(googleAuthId);
}

498
src/db/publicCatalog.js Normal file
View File

@@ -0,0 +1,498 @@
import { getProvider } from './store.js';
export async function getCompanyCatalogMeta(companyId) {
const provider = await getProvider();
const result = await provider.query(
`SELECT
id,
name,
root_category_id AS rootCategoryId,
default_language_id AS languageId,
poscustomergroup_id AS posCustomerGroupId
FROM company
WHERE id = @companyId`,
{ companyId }
);
return result.rows[0] ?? null;
}
export async function listCategories(companyId) {
const company = await getCompanyCatalogMeta(companyId);
if (!company) {
throw new Error('Company not found');
}
if (!company.rootCategoryId) {
return [];
}
const provider = await getProvider();
const isSqlite = provider.dialect === 'sqlite3';
const sql = isSqlite
? `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`
: `WITH 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
OUTER APPLY (
SELECT TOP 1 cp.pic_id
FROM categorypic cp
WHERE cp.category_id = t.id
ORDER BY cp.sort ASC, cp.id ASC
) first_pic
LEFT JOIN pic p ON p.id = first_pic.pic_id
ORDER BY t.sort, t.id`;
const result = await provider.query(sql, {
rootId: company.rootCategoryId,
languageId: company.languageId,
});
return result.rows.map((row) => ({
id: row.id,
parentId: row.parentId,
sort: row.sort,
name: row.name ?? '',
slug: row.slug ?? '',
fullHash: row.fullHash ?? null,
thumbW: row.thumbW ?? null,
thumbH: row.thumbH ?? null,
thumbHash: row.thumbHash ?? null,
}));
}
export async function listArticles(companyId, { categoryId, limit = 50, offset = 0, search } = {}) {
const company = await getCompanyCatalogMeta(companyId);
if (!company) {
throw new Error('Company not found');
}
const provider = await getProvider();
const isSqlite = provider.dialect === 'sqlite3';
const safeLimit = Math.min(Math.max(1, Number(limit) || 50), 200);
const safeOffset = Math.max(0, Number(offset) || 0);
const trimmedSearch = typeof search === 'string' ? search.trim() : '';
const params = {
companyId,
languageId: company.languageId,
posCustomerGroupId: company.posCustomerGroupId,
limit: safeLimit,
offset: safeOffset,
};
if (categoryId != null) {
params.categoryId = Number(categoryId);
}
if (trimmedSearch) {
params.searchPattern = `%${trimmedSearch}%`;
}
const whereClauses = [
'a.company_id = @companyId',
'a.fatherarticle_id IS NULL',
];
if (categoryId != null) {
whereClauses.push('ca.category_id = @categoryId');
}
if (trimmedSearch) {
whereClauses.push('(a.sku LIKE @searchPattern OR at.name LIKE @searchPattern)');
}
const whereSql = whereClauses.join(' AND ');
const fromClause = categoryId != null
? 'categoryarticle ca INNER JOIN article a ON a.id = ca.article_id'
: 'article a';
const sql = isSqlite
? `SELECT
a.id,
a.sku,
at.name,
at.slug,
pr.net,
t.rate AS taxRate,
a.track_stock AS trackStock,
pic.hash AS fullHash,
pic.thumbw AS thumbW,
pic.thumbh AS thumbH,
pic.thumbhash AS thumbHash,
b.id AS brandId,
b.name AS brandName
FROM ${fromClause}
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 brand b ON b.id = a.brand_id
WHERE ${whereSql}
ORDER BY a.id
LIMIT @limit OFFSET @offset`
: `SELECT
a.id,
a.sku,
at.name,
at.slug,
pr.net,
t.rate AS taxRate,
a.track_stock AS trackStock,
pic.hash AS fullHash,
pic.thumbw AS thumbW,
pic.thumbh AS thumbH,
pic.thumbhash AS thumbHash,
b.id AS brandId,
b.name AS brandName
FROM ${fromClause}
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
OUTER APPLY (
SELECT TOP 1 ap.pic_id
FROM articlepic ap
WHERE ap.article_id = a.id
ORDER BY ap.sort ASC, ap.id ASC
) first_pic
LEFT JOIN pic ON pic.id = first_pic.pic_id
LEFT JOIN brand b ON b.id = a.brand_id
WHERE ${whereSql}
ORDER BY a.id
OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`;
const result = await provider.query(sql, params);
return result.rows.map((row) => {
const net = row.net != null ? Number(row.net) : null;
const taxRate = row.taxRate != null ? Number(row.taxRate) : null;
const gross = net != null && taxRate != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
return {
id: row.id,
sku: row.sku,
name: row.name ?? '',
slug: row.slug ?? '',
net,
taxRate,
gross,
trackStock: Boolean(row.trackStock ?? 1),
brand: row.brandName
? {
id: row.brandId ?? null,
name: row.brandName,
}
: null,
fullHash: row.fullHash ?? null,
thumbW: row.thumbW ?? null,
thumbH: row.thumbH ?? null,
thumbHash: row.thumbHash ?? null,
};
});
}
export async function getArticleBySku(companyId, sku) {
const company = await getCompanyCatalogMeta(companyId);
if (!company) {
throw new Error('Company not found');
}
const provider = await getProvider();
const isSqlite = provider.dialect === 'sqlite3';
const sql = isSqlite
? `SELECT
a.id,
a.sku,
at.name,
at.slug,
at.desc AS description,
a.unitamount,
a.weight,
a.track_stock AS trackStock,
u.si AS unitSi,
u.basesi AS unitBasesi,
u.basesifactor AS unitBasesifactor,
ut.symbol AS unitSymbol,
ut.name AS unitName,
b.id AS brandId,
b.name AS brandName,
brand_pic.hash AS brandFullHash,
brand_pic.thumbw AS brandThumbW,
brand_pic.thumbh AS brandThumbH,
brand_pic.thumbhash AS brandThumbHash,
a.is_father AS isFather,
a.fatherarticle_id AS fatherArticleId,
father.sku AS fatherSku
FROM article a
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
WHERE a.sku = @sku AND a.company_id = @companyId`
: `SELECT
a.id,
a.sku,
at.name,
at.slug,
at.desc AS description,
a.unitamount,
a.weight,
a.track_stock AS trackStock,
u.si AS unitSi,
u.basesi AS unitBasesi,
u.basesifactor AS unitBasesifactor,
ut.symbol AS unitSymbol,
ut.name AS unitName,
b.id AS brandId,
b.name AS brandName,
brand_pic.hash AS brandFullHash,
brand_pic.thumbw AS brandThumbW,
brand_pic.thumbh AS brandThumbH,
brand_pic.thumbhash AS brandThumbHash,
a.is_father AS isFather,
a.fatherarticle_id AS fatherArticleId,
father.sku AS fatherSku
FROM article a
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
OUTER APPLY (
SELECT TOP 1 bp.pic_id
FROM brandpic bp
WHERE bp.brand_id = b.id
ORDER BY bp.sort ASC, bp.id ASC
) first_brand_pic
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
LEFT JOIN article father ON father.id = a.fatherarticle_id
WHERE a.sku = @sku AND a.company_id = @companyId`;
const result = await provider.query(sql, {
sku,
companyId,
languageId: company.languageId,
});
const row = result.rows[0];
if (!row) {
throw new Error('Article not found');
}
const articleId = row.id;
const [categoriesRes, photosRes, eansRes, pricesRes, variantsRes] = await Promise.all([
provider.query(
`SELECT ca.category_id AS categoryId, ct.name, ct.slug
FROM categoryarticle ca
LEFT JOIN categorytext ct ON ct.category_id = ca.category_id AND ct.language_id = @languageId
WHERE ca.article_id = @articleId
ORDER BY ca.category_id`,
{ articleId, languageId: company.languageId }
),
provider.query(
`SELECT
ap.sort,
p.hash AS fullHash,
p.w AS fullW,
p.h AS fullH,
p.size AS fullSize,
p.thumbw AS thumbW,
p.thumbh AS thumbH,
p.thumbhash AS thumbHash
FROM articlepic ap
INNER JOIN pic p ON p.id = ap.pic_id
WHERE ap.article_id = @articleId
ORDER BY ap.sort ASC, ap.id ASC`,
{ articleId }
),
provider.query(
`SELECT ae.ean
FROM articleean ae
WHERE ae.article_id = @articleId
ORDER BY ae.ean`,
{ articleId }
),
provider.query(
`SELECT
pr.minmultiplier,
pr.net,
t.rate AS taxRate
FROM price pr
INNER JOIN tax t ON t.id = pr.tax_id
WHERE pr.article_id = @articleId
AND pr.customergroup_id = @posCustomerGroupId
ORDER BY pr.minmultiplier ASC`,
{ articleId, posCustomerGroupId: company.posCustomerGroupId }
),
row.isFather
? provider.query(
`SELECT
child.id,
child.sku,
child.fathersort,
child.track_stock AS trackStock,
child.unitamount,
child.weight,
at_child.name,
at_child.slug,
av.id AS attributeValueId,
av.value AS attributeValue
FROM article child
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id
WHERE child.fatherarticle_id = @articleId
ORDER BY child.fathersort ASC, child.id ASC`,
{ articleId, languageId: company.languageId }
)
: Promise.resolve({ rows: [] }),
]);
const prices = pricesRes.rows.map((p) => {
const net = p.net != null ? Number(p.net) : null;
const taxRate = p.taxRate != null ? Number(p.taxRate) : null;
const gross = net != null && taxRate != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
return {
minmultiplier: p.minmultiplier,
net,
taxRate,
gross,
};
});
const primaryPrice = prices[0] ?? null;
return {
id: row.id,
sku: row.sku,
name: row.name ?? '',
slug: row.slug ?? '',
description: row.description ?? '',
unitamount: row.unitamount,
weight: row.weight,
trackStock: Boolean(row.trackStock ?? 1),
unit: row.unitSi
? {
si: row.unitSi,
basesi: row.unitBasesi,
basesifactor: row.unitBasesifactor,
symbol: row.unitSymbol ?? null,
name: row.unitName ?? null,
}
: null,
brand: row.brandName
? {
id: row.brandId ?? null,
name: row.brandName,
fullHash: row.brandFullHash ?? null,
thumbW: row.brandThumbW ?? null,
thumbH: row.brandThumbH ?? null,
thumbHash: row.brandThumbHash ?? null,
}
: null,
price: primaryPrice,
prices,
categories: categoriesRes.rows.map((c) => ({
id: c.categoryId,
name: c.name ?? '',
slug: c.slug ?? '',
})),
photos: photosRes.rows.map((p) => ({
sort: p.sort,
fullHash: p.fullHash,
fullW: p.fullW,
fullH: p.fullH,
fullSize: p.fullSize,
thumbW: p.thumbW,
thumbH: p.thumbH,
thumbHash: p.thumbHash,
})),
eans: eansRes.rows.map((e) => e.ean),
isFather: Boolean(row.isFather),
fatherSku: row.fatherSku ?? null,
variants: variantsRes.rows.map((v) => ({
id: v.id,
sku: v.sku,
sort: v.fathersort,
name: v.name ?? '',
slug: v.slug ?? '',
attributeValueId: v.attributeValueId ?? null,
attributeValue: v.attributeValue ?? null,
unitamount: v.unitamount,
weight: v.weight,
trackStock: Boolean(v.trackStock ?? 1),
})),
};
}

View File

@@ -44,6 +44,8 @@ const STORE_NAMESPACES = [
'attributevalue',
'articleattributevalue',
'categoryattributevalue',
'modelconversation',
'modelmessage',
];
/**
@@ -148,6 +150,11 @@ export async function getStore() {
return connecting;
}
export async function getProvider() {
await getStore();
return provider;
}
export async function ensureConnection() {
await getStore();
}
@@ -159,3 +166,4 @@ export async function closeStore() {
store = undefined;
}
}