Compare commits

..

5 Commits

Author SHA1 Message Date
seb
00cf5b418e u 2026-10-09 20:05:25 +02:00
seb
f840a12b78 u 2026-10-09 19:02:19 +02:00
seb
178055f1d8 u 2026-10-09 18:26:01 +02:00
seb
07c4f0bff9 u 2026-10-09 18:10:51 +02:00
seb
4ae3439289 u 2026-10-09 17:55:47 +02:00
22 changed files with 3118 additions and 2430 deletions

View File

@@ -32,7 +32,7 @@ Adds a child article to a parent article (creates and attaches with auto-generat
*Source: `src/store/article/addImage.js`*
Adds an image to an article by its SKU, processing it into AVIF variants.
Adds an image to an article by its SKU, processing full, thumb, and 40px-edge small-thumb AVIF variants.
**Parameters**
@@ -45,13 +45,13 @@ Adds an image to an article by its SKU, processing it into AVIF variants.
**Returns**
`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info.
`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info.
**Example**
```js
const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
// => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
// => { picId, sort, full: { w, h, size }, thumb: { w, h }, smallThumb: { w, h }, fullHash, thumbHash }
```
## `article.attachChild`

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/brand/addImage.js`*
Adds an image to a brand, processing it into AVIF full and thumb variants.
Adds an image to a brand, processing full, thumb, and 40px-edge small-thumb AVIF variants.
**Parameters**
@@ -19,7 +19,38 @@ Adds an image to a brand, processing it into AVIF full and thumb variants.
**Returns**
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array<object>}>`
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
## `brand.create`
*Source: `src/store/brand/create.js`*
Creates a brand for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | The brand input. |
| `input.name` | `string` | The brand name. |
| `[input.gsprpinfo]` | `string` | Optional GSPRP info. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The created brand.
**Throws**
`Error` — If the brand is invalid or already exists.
**Example**
```js
const brand = await store.brand.create(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
// => { id, name, gsprpinfo, pics: [] }
```
## `brand.delete`
@@ -53,24 +84,60 @@ await store.brand.delete(42, 3);
*Source: `src/store/brand/list.js`*
Lists the brands of the user's selected company.
Lists the brands of the user's selected company, sorted by name.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[opts]` | `object` | Optional pagination options. |
| `[opts.offset=0]` | `number` | Number of brands to skip. |
| `[opts.limit=10]` | `number` | Number of brands to return (max 100). |
| `[opts.includeSmallThumbs=false]` | `boolean` | Include 40px-edge small thumbnail dimensions and base64 AVIF data. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>` — The brands.
`Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>` — The brands plus the total unpaged count. Small-thumbnail fields are present only when `includeSmallThumbs` is true; the binary is base64-encoded AVIF data.
**Example**
```js
const brands = await store.brand.list(42);
// => [{ id, name, gsprpinfo, pics: [{ sort, fullHash, thumbW, thumbH, thumbHash }] }]
const result = await store.brand.list(42, { offset: 0, limit: 10 });
// => { brands: [{ id, name, gsprpinfo, pics: [...] }], total: 25, offset: 0, limit: 10 }
```
## `brand.update`
*Source: `src/store/brand/update.js`*
Renames/updates a brand for the user's selected company by id. Touching `article.updated_at` for all articles linked via `brand_id` when the name actually changes, so list caches invalidate.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The id of the brand to update. |
| `input` | `object` | The update input. |
| `[input.name]` | `string` | The new brand name. |
| `[input.gsprpinfo]` | `string` | The new GSPRP info. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>` — The updated brand plus the number of linked articles whose `updated_at` was bumped.
**Throws**
`Error` — If the brand is not found, invalid, or the name is already taken.
**Example**
```js
const brand = await store.brand.update(42, 3, { name: 'Coca-Cola GmbH' });
// => { id: 3, name, gsprpinfo, touchedArticleCount: 2 }
```
## `brand.upsert`
@@ -91,7 +158,7 @@ Creates or updates a brand for the user's selected company, keyed by name.
**Returns**
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand.
`Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` — The upserted brand.
**Throws**

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/category/addImage.js`*
Adds an image to a category, processing it into AVIF full and thumb variants.
Adds an image to a category, processing full, thumb, and 40px-edge small-thumb AVIF variants.
**Parameters**
@@ -19,7 +19,7 @@ Adds an image to a category, processing it into AVIF full and thumb variants.
**Returns**
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` — The added image info.
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>` — The added image info.
**Example**

12
API.md
View File

@@ -74,7 +74,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `list(googleAuthId: number, tx?: object): Promise<Array<object>>`
- `update(id: number, input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>`
- [category](API.category.md)
- `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>`
- `addImage(googleAuthId: number, categoryId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>`
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}, tx?: object): Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>`
- `delete(input: {googleAuthId: number, id: number, recursive?: boolean}, tx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>`
@@ -86,7 +86,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>`
- [article](API.article.md)
- `addChild(googleAuthId: number, parentSku: string, options: {attach?: boolean, attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>`
- `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>`
- `attachChild(googleAuthId: number, parentSku: string, options: {sku: string, childSku?: string, attributeValueId?: number, value?: string, reparent?: boolean, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<{article: object, children: Array<object>}>`
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>`
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
@@ -129,10 +129,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>`
- `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>`
- [brand](API.brand.md)
- `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array<object>}>`
- `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>`
- `create(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>`
- `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, includeSmallThumbs?: boolean}, tx?: object): Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>`
- `update(googleAuthId: number, id: number, input: {name?: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>`
- `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>`
- [customergroup](API.customergroup.md)
- `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string, sort: number}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 312 KiB

View File

@@ -28,7 +28,7 @@ async function runTests() {
assert(existsSync(join(storageDir, 'fullbinary')), 'fullbinary dir should exist');
assert(existsSync(join(storageDir, 'thumbbinary')), 'thumbbinary dir should exist');
// Verify sqlite tables do NOT contain blob columns
// Large file and image blobs stay on disk; the 40px small-thumb stays inline in pic.
const fileTableInfo = provider.db.pragma('table_info("file")');
const fileColNames = fileTableInfo.map((c) => c.name);
console.log('file table columns:', fileColNames);
@@ -40,6 +40,9 @@ async function runTests() {
console.log('pic table columns:', picColNames);
assert(!picColNames.includes('fullbinary'), 'pic table should NOT have fullbinary column');
assert(!picColNames.includes('thumbbinary'), 'pic table should NOT have thumbbinary column');
assert(picColNames.includes('smallthumbw'), 'pic table should have smallthumbw column');
assert(picColNames.includes('smallthumbh'), 'pic table should have smallthumbh column');
assert(picColNames.includes('smallthumbbinary'), 'pic table should have smallthumbbinary column');
// Setup base data: google_auth, company, unit, article, manifest
const googleAuthId = await store.google_auth.add('test@example.com');
@@ -199,6 +202,42 @@ async function runTests() {
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
const fullHash = picResult.fullHash;
assert.equal(picResult.smallThumb.w, 40);
assert.equal(picResult.smallThumb.h, 27);
const smallThumbRow = provider.db.prepare(
'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?',
).get(fullHash);
assert.equal(smallThumbRow.smallthumbw, 40);
assert.equal(smallThumbRow.smallthumbh, 27);
assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row');
const smallThumbMetadata = await sharp(smallThumbRow.smallthumbbinary).metadata();
assert.equal(smallThumbMetadata.format, 'heif');
assert.equal(smallThumbMetadata.width, 40);
assert.equal(smallThumbMetadata.height, 27);
const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' });
await store.brand.addImage(googleAuthId, brand.id, sampleImage);
const listedBrands = await store.brand.list(googleAuthId, { offset: 0, limit: 100 });
const listedBrand = listedBrands.brands.find((item) => item.id === brand.id);
assert(listedBrand, 'Brand list should include the created brand');
assert.equal(listedBrand.pics.length, 1);
assert(!('smallThumbW' in listedBrand.pics[0]), 'Small-thumb fields should be omitted by default');
const listedBrandsWithSmallThumbs = await store.brand.list(googleAuthId, {
offset: 0,
limit: 100,
includeSmallThumbs: true,
});
const listedBrandWithSmallThumbs = listedBrandsWithSmallThumbs.brands.find((item) => item.id === brand.id);
assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand');
assert.equal(listedBrandWithSmallThumbs.pics.length, 1);
const listedPic = listedBrandWithSmallThumbs.pics[0];
assert.equal(listedPic.smallThumbW, 40);
assert.equal(listedPic.smallThumbH, 27);
assert.equal(typeof listedPic.smallThumbBinary, 'string');
const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64');
assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary);
console.log('Article pic added with hash:', fullHash);
const fullPath = join(storageDir, 'fullbinary', fullHash + '.avif');

View File

@@ -47,6 +47,8 @@ import companysettingList from './store/companysetting/list.js';
import companysettingGet from './store/companysetting/get.js';
import companysettingSet from './store/companysetting/set.js';
import brandList from './store/brand/list.js';
import brandCreate from './store/brand/create.js';
import brandUpdate from './store/brand/update.js';
import brandUpsert from './store/brand/upsert.js';
import brandDelete from './store/brand/delete.js';
import brandAddImage from './store/brand/addImage.js';
@@ -326,6 +328,8 @@ export default class LibDashStore {
this.brand = {
list: brandList(provider),
create: brandCreate(provider),
update: brandUpdate(provider),
upsert: brandUpsert(provider),
delete: brandDelete(provider),
addImage: brandAddImage(provider),

View File

@@ -5,8 +5,6 @@ SELECT
b.gsprpinfo,
(
SELECT
bp.id,
bp.sort,
p.hash AS fullHash,
p.thumbw AS thumbW,
p.thumbh AS thumbH,
@@ -22,6 +20,61 @@ WHERE b.company_id = @companyId
ORDER BY b.name
`;
export const COUNT_BY_COMPANY = `
SELECT COUNT(*) AS total
FROM brand b
WHERE b.company_id = @companyId
`;
export const LIST_BY_COMPANY_PAGED = `
SELECT
b.id,
b.name,
b.gsprpinfo,
(
SELECT
p.hash AS fullHash,
p.thumbw AS thumbW,
p.thumbh AS thumbH,
p.thumbhash AS thumbHash
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id
ORDER BY bp.sort ASC, bp.id ASC
FOR JSON PATH
) AS picsJson
FROM brand b
WHERE b.company_id = @companyId
ORDER BY b.name
OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY
`;
export const LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS = `
SELECT
b.id,
b.name,
b.gsprpinfo,
(
SELECT
p.hash AS fullHash,
p.thumbw AS thumbW,
p.thumbh AS thumbH,
p.thumbhash AS thumbHash,
p.smallthumbw AS smallThumbW,
p.smallthumbh AS smallThumbH,
p.smallthumbbinary AS smallThumbBinary
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id
ORDER BY bp.sort ASC, bp.id ASC
FOR JSON PATH
) AS picsJson
FROM brand b
WHERE b.company_id = @companyId
ORDER BY b.name
OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY
`;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
b.id,
@@ -29,8 +82,6 @@ SELECT
b.gsprpinfo,
(
SELECT
bp.id,
bp.sort,
p.hash AS fullHash,
p.thumbw AS thumbW,
p.thumbh AS thumbH,

View File

@@ -1,10 +1,14 @@
export const CREATE = `
INSERT INTO pic (
w, h, size, hash, thumbw, thumbh, thumbhash, source, fullbinary, thumbbinary
w, h, size, hash, thumbw, thumbh, thumbhash,
smallthumbw, smallthumbh, smallthumbbinary,
source, fullbinary, thumbbinary
)
OUTPUT INSERTED.id
VALUES (
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source, @fullbinary, @thumbbinary
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash,
@smallthumbw, @smallthumbh, @smallthumbbinary,
@source, @fullbinary, @thumbbinary
)
`;

View File

@@ -95,9 +95,12 @@ BEGIN
thumbw INT NOT NULL,
thumbh INT NOT NULL,
thumbhash NVARCHAR(64) NOT NULL,
smallthumbw INT NOT NULL,
smallthumbh INT NOT NULL,
source NVARCHAR(MAX) NOT NULL,
fullbinary VARBINARY(MAX) NOT NULL,
thumbbinary VARBINARY(MAX) NOT NULL,
smallthumbbinary VARBINARY(MAX) NOT NULL,
CONSTRAINT UQ_pic_hash UNIQUE (hash)
);
END
@@ -1220,4 +1223,3 @@ export const SCHEMA_SCRIPTS = [
INIT_SHIPPING,
INIT_PACKAGE,
];

View File

@@ -4,6 +4,7 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'brand');
export const {
CREATE, DELETE_BY_COMPANY_AND_ID, UPDATE_BY_ID, GET_BY_ID_AND_COMPANY,
COUNT_BY_COMPANY,
} = queries;
export const LIST_BY_COMPANY = `
@@ -13,15 +14,13 @@ SELECT
b.gsprpinfo,
(
SELECT COALESCE(json_group_array(json_object(
'id', id,
'sort', sort,
'fullHash', hash,
'thumbW', thumbw,
'thumbH', thumbh,
'thumbHash', thumbhash
)), '[]')
FROM (
SELECT bp.id, bp.sort, p.hash, p.thumbw, p.thumbh, p.thumbhash
SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id
@@ -33,6 +32,65 @@ WHERE b.company_id = @companyId
ORDER BY b.name
`;
export const LIST_BY_COMPANY_PAGED = `
SELECT
b.id,
b.name,
b.gsprpinfo,
(
SELECT COALESCE(json_group_array(json_object(
'fullHash', hash,
'thumbW', thumbw,
'thumbH', thumbh,
'thumbHash', thumbhash
)), '[]')
FROM (
SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id
ORDER BY bp.sort, bp.id
)
) AS picsJson
FROM brand b
WHERE b.company_id = @companyId
ORDER BY b.name
LIMIT @limit OFFSET @offset
`;
export const LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS = `
SELECT
b.id,
b.name,
b.gsprpinfo,
(
SELECT COALESCE(json_group_array(json_object(
'fullHash', hash,
'thumbW', thumbw,
'thumbH', thumbh,
'thumbHash', thumbhash,
'smallThumbW', smallthumbw,
'smallThumbH', smallthumbh,
'smallThumbBinaryHex', CASE
WHEN smallthumbbinary IS NULL THEN NULL
ELSE hex(smallthumbbinary)
END
)), '[]')
FROM (
SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash,
p.smallthumbw, p.smallthumbh, p.smallthumbbinary
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id
ORDER BY bp.sort, bp.id
)
) AS picsJson
FROM brand b
WHERE b.company_id = @companyId
ORDER BY b.name
LIMIT @limit OFFSET @offset
`;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
b.id,
@@ -40,15 +98,13 @@ SELECT
b.gsprpinfo,
(
SELECT COALESCE(json_group_array(json_object(
'id', id,
'sort', sort,
'fullHash', hash,
'thumbW', thumbw,
'thumbH', thumbh,
'thumbHash', thumbhash
)), '[]')
FROM (
SELECT bp.id, bp.sort, p.hash, p.thumbw, p.thumbh, p.thumbhash
SELECT p.hash, p.thumbw, p.thumbh, p.thumbhash
FROM brandpic bp
INNER JOIN pic p ON p.id = bp.pic_id
WHERE bp.brand_id = b.id

View File

@@ -8,10 +8,12 @@ export const {
export const CREATE = `
INSERT INTO pic (
w, h, size, hash, thumbw, thumbh, thumbhash, source
w, h, size, hash, thumbw, thumbh, thumbhash,
smallthumbw, smallthumbh, smallthumbbinary, source
)
VALUES (
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash,
@smallthumbw, @smallthumbh, @smallthumbbinary, @source
)
RETURNING id
`;

View File

@@ -10,14 +10,14 @@ INSERT OR IGNORE INTO meta ("key", value) VALUES ('version', '1');
export const INIT_LANGUAGE = `
CREATE TABLE IF NOT EXISTS language (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
iso639 TEXT NOT NULL COLLATE NOCASE UNIQUE
);
`;
export const INIT_TAX = `
CREATE TABLE IF NOT EXISTS tax (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
rate NUMERIC NOT NULL UNIQUE
);
INSERT OR IGNORE INTO tax (rate) VALUES (0), (7), (19);
@@ -25,7 +25,7 @@ INSERT OR IGNORE INTO tax (rate) VALUES (0), (7), (19);
export const INIT_CATEGORY = `
CREATE TABLE IF NOT EXISTS category (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER,
sort INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS category (
export const INIT_PIC = `
CREATE TABLE IF NOT EXISTS pic (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
w INTEGER NOT NULL,
h INTEGER NOT NULL,
size INTEGER NOT NULL,
@@ -43,13 +43,16 @@ CREATE TABLE IF NOT EXISTS pic (
thumbw INTEGER NOT NULL,
thumbh INTEGER NOT NULL,
thumbhash TEXT NOT NULL,
smallthumbw INTEGER NOT NULL,
smallthumbh INTEGER NOT NULL,
smallthumbbinary BLOB NOT NULL,
source TEXT NOT NULL
);
`;
export const INIT_COMPANY = `
CREATE TABLE IF NOT EXISTS company (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL COLLATE NOCASE UNIQUE,
root_category_id INTEGER,
default_language_id INTEGER,
@@ -68,7 +71,7 @@ CREATE TABLE IF NOT EXISTS company (
export const INIT_GOOGLE_AUTH = `
CREATE TABLE IF NOT EXISTS google_auth (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL COLLATE NOCASE UNIQUE,
picture TEXT,
picturebinary BLOB,
@@ -83,7 +86,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS UQ_google_auth_sub
export const INIT_CUSTOMERGROUP = `
CREATE TABLE IF NOT EXISTS customergroup (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
company_id INTEGER,
@@ -97,7 +100,7 @@ export const INIT_COMPANY_POS_CUSTOMERGROUP_FK = '';
export const INIT_ENTITY = `
CREATE TABLE IF NOT EXISTS entity (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
customergroup_id INTEGER NOT NULL,
company_id INTEGER,
first_name TEXT,
@@ -140,7 +143,7 @@ END;
export const INIT_ENTITYADDRESS = `
CREATE TABLE IF NOT EXISTS entityaddress (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_id INTEGER NOT NULL,
first_name TEXT,
name TEXT,
@@ -157,7 +160,7 @@ CREATE TABLE IF NOT EXISTS entityaddress (
export const INIT_ENTITYCONTACT = `
CREATE TABLE IF NOT EXISTS entitycontact (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_id INTEGER NOT NULL,
contacttype TEXT NOT NULL,
contact TEXT NOT NULL,
@@ -168,7 +171,7 @@ CREATE TABLE IF NOT EXISTS entitycontact (
export const INIT_UNIT = `
CREATE TABLE IF NOT EXISTS unit (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
si TEXT NOT NULL,
basesi TEXT NOT NULL,
@@ -180,7 +183,7 @@ CREATE TABLE IF NOT EXISTS unit (
export const INIT_UNITTEXT = `
CREATE TABLE IF NOT EXISTS unittext (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
unit_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
symbol TEXT NOT NULL,
@@ -192,16 +195,16 @@ CREATE TABLE IF NOT EXISTS unittext (
`;
export const INIT_OPTIONSLIST = `
CREATE TABLE IF NOT EXISTS optionslist (id INTEGER PRIMARY KEY);
CREATE TABLE IF NOT EXISTS optionslist (id INTEGER PRIMARY KEY AUTOINCREMENT);
`;
export const INIT_ARTICLELIST = `
CREATE TABLE IF NOT EXISTS articlelist (id INTEGER PRIMARY KEY);
CREATE TABLE IF NOT EXISTS articlelist (id INTEGER PRIMARY KEY AUTOINCREMENT);
`;
export const INIT_BRAND = `
CREATE TABLE IF NOT EXISTS brand (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL COLLATE NOCASE,
gsprpinfo TEXT,
@@ -212,7 +215,7 @@ CREATE TABLE IF NOT EXISTS brand (
export const INIT_ARTICLE = `
CREATE TABLE IF NOT EXISTS article (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
sku TEXT COLLATE NOCASE,
unit_id INTEGER NOT NULL,
@@ -244,7 +247,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_parentarticle_parentsort
export const INIT_ARTICLEEAN = `
CREATE TABLE IF NOT EXISTS articleean (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
ean TEXT NOT NULL,
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE
@@ -257,7 +260,7 @@ CREATE INDEX IF NOT EXISTS IX_articleean_ean
export const INIT_ARTICLEPIC = `
CREATE TABLE IF NOT EXISTS articlepic (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
pic_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -270,7 +273,7 @@ CREATE TABLE IF NOT EXISTS articlepic (
export const INIT_BRANDPIC = `
CREATE TABLE IF NOT EXISTS brandpic (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand_id INTEGER NOT NULL,
pic_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -283,7 +286,7 @@ CREATE TABLE IF NOT EXISTS brandpic (
export const INIT_ARTICLELISTARTICLE = `
CREATE TABLE IF NOT EXISTS articlelistarticle (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
articlelist_id INTEGER NOT NULL,
article_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
@@ -297,7 +300,7 @@ CREATE TABLE IF NOT EXISTS articlelistarticle (
export const INIT_OPTIONSLISTARTICLE = `
CREATE TABLE IF NOT EXISTS optionslistarticle (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
optionslist_id INTEGER NOT NULL,
article_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
@@ -312,7 +315,7 @@ CREATE TABLE IF NOT EXISTS optionslistarticle (
export const INIT_CATEGORYARTICLE = `
CREATE TABLE IF NOT EXISTS categoryarticle (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
article_id INTEGER NOT NULL,
UNIQUE (category_id, article_id),
@@ -325,7 +328,7 @@ CREATE INDEX IF NOT EXISTS IX_categoryarticle_article_category
export const INIT_CATEGORYPIC = `
CREATE TABLE IF NOT EXISTS categorypic (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
pic_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -338,7 +341,7 @@ CREATE TABLE IF NOT EXISTS categorypic (
export const INIT_CATEGORYTEXT = `
CREATE TABLE IF NOT EXISTS categorytext (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
name TEXT NOT NULL,
@@ -353,7 +356,7 @@ CREATE TABLE IF NOT EXISTS categorytext (
export const INIT_ARTICLENAME = `
CREATE TABLE IF NOT EXISTS articletext (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
language_id INTEGER NOT NULL,
name TEXT NOT NULL,
@@ -368,7 +371,7 @@ CREATE TABLE IF NOT EXISTS articletext (
export const INIT_PRICE = `
CREATE TABLE IF NOT EXISTS price (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
tax_id INTEGER NOT NULL,
article_id INTEGER NOT NULL,
minmultiplier NUMERIC NOT NULL DEFAULT 1,
@@ -384,7 +387,7 @@ CREATE INDEX IF NOT EXISTS IX_price_article_cg_mult
export const INIT_ARTICLE_SUPPLIER = `
CREATE TABLE IF NOT EXISTS articlesupplier (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
entity_id INTEGER NOT NULL,
net REAL,
@@ -405,7 +408,7 @@ CREATE INDEX IF NOT EXISTS IX_articlesupplier_article_entity
export const INIT_MANIFEST = `
CREATE TABLE IF NOT EXISTS manifest (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
"number" TEXT NOT NULL,
"type" TEXT NOT NULL,
@@ -420,7 +423,7 @@ CREATE TABLE IF NOT EXISTS manifest (
export const INIT_MANIFESTARTICLE = `
CREATE TABLE IF NOT EXISTS manifestarticle (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
manifest_id INTEGER NOT NULL,
sku TEXT,
name TEXT,
@@ -436,7 +439,7 @@ CREATE TABLE IF NOT EXISTS manifestarticle (
export const INIT_WAREHOUSE = `
CREATE TABLE IF NOT EXISTS warehouse (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL COLLATE NOCASE,
sort INTEGER NOT NULL DEFAULT 0,
@@ -448,7 +451,7 @@ CREATE TABLE IF NOT EXISTS warehouse (
/* 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,
id INTEGER PRIMARY KEY AUTOINCREMENT,
manifestarticle_id INTEGER NOT NULL,
warehouse_id INTEGER,
transaction_type TEXT NOT NULL,
@@ -464,7 +467,7 @@ CREATE TABLE IF NOT EXISTS ledger (
export const INIT_NUMBERS = `
CREATE TABLE IF NOT EXISTS numbers (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
"type" TEXT NOT NULL,
prefix TEXT NOT NULL DEFAULT '',
@@ -477,7 +480,7 @@ CREATE TABLE IF NOT EXISTS numbers (
export const INIT_COMPANYUSER = `
CREATE TABLE IF NOT EXISTS companyuser (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
google_auth_id INTEGER NOT NULL,
company_id INTEGER NOT NULL,
UNIQUE (google_auth_id, company_id),
@@ -488,7 +491,7 @@ CREATE TABLE IF NOT EXISTS companyuser (
export const INIT_MODELCONVERSATION = `
CREATE TABLE IF NOT EXISTS modelconversation (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
companyuser_id INTEGER NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
@@ -503,7 +506,7 @@ CREATE INDEX IF NOT EXISTS IX_modelconversation_companyuser ON modelconversation
export const INIT_MODELVFS = `
CREATE TABLE IF NOT EXISTS modelvfs (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
mime TEXT NOT NULL,
@@ -518,7 +521,7 @@ CREATE INDEX IF NOT EXISTS IX_modelvfs_company_name ON modelvfs (company_id, nam
export const INIT_MODELMESSAGE = `
CREATE TABLE IF NOT EXISTS modelmessage (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id INTEGER NOT NULL,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool', 'reasoning', 'usage')),
@@ -530,7 +533,7 @@ CREATE INDEX IF NOT EXISTS IX_modelmessage_conversation ON modelmessage (convers
export const INIT_ARTICLEHISTORY = `
CREATE TABLE IF NOT EXISTS articlehistory (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
companyuser_id INTEGER,
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
@@ -541,7 +544,7 @@ CREATE TABLE IF NOT EXISTS articlehistory (
`;
export const INIT_COMPANYHISTORY = `
CREATE TABLE IF NOT EXISTS companyhistory (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
companyuser_id INTEGER,
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
@@ -553,7 +556,7 @@ CREATE TABLE IF NOT EXISTS companyhistory (
export const INIT_ARTICLETOMBSTONE = `
CREATE TABLE IF NOT EXISTS articletombstone (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
article_id INTEGER,
sku TEXT COLLATE NOCASE,
@@ -570,7 +573,7 @@ CREATE INDEX IF NOT EXISTS IX_articletombstone_company_deleted
export const INIT_ENTITYHISTORY = `
CREATE TABLE IF NOT EXISTS entityhistory (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_id INTEGER NOT NULL,
companyuser_id INTEGER,
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
@@ -582,7 +585,7 @@ CREATE TABLE IF NOT EXISTS entityhistory (
export const INIT_COMPANYSETTING = `
CREATE TABLE IF NOT EXISTS companysetting (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
"key" TEXT NOT NULL,
value TEXT NOT NULL,
@@ -595,7 +598,7 @@ CREATE TABLE IF NOT EXISTS companysetting (
export const INIT_POSTERMINAL = `
CREATE TABLE IF NOT EXISTS posterminal (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
token TEXT,
@@ -616,7 +619,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS UQ_posterminal_token
export const INIT_POSTERMINALCATEGORY = `
CREATE TABLE IF NOT EXISTS posterminalcategory (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
posterminal_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
UNIQUE (posterminal_id, category_id),
@@ -627,7 +630,7 @@ CREATE TABLE IF NOT EXISTS posterminalcategory (
export const INIT_PRINTER = `
CREATE TABLE IF NOT EXISTS printer (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
address TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('esc/pos', 'zpl', 'ipp')),
@@ -642,7 +645,7 @@ CREATE TABLE IF NOT EXISTS printer (
export const INIT_LABELTEMPLATE = `
CREATE TABLE IF NOT EXISTS labeltemplate (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
width INTEGER NOT NULL,
@@ -657,7 +660,7 @@ CREATE TABLE IF NOT EXISTS labeltemplate (
export const INIT_FILE = `
CREATE TABLE IF NOT EXISTS "file" (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT NOT NULL,
mimetype TEXT NOT NULL,
name TEXT NOT NULL,
@@ -672,7 +675,7 @@ CREATE TABLE IF NOT EXISTS "file" (
export const INIT_MANIFESTATTACHMENT = `
CREATE TABLE IF NOT EXISTS manifestattachment (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
manifest_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -696,7 +699,7 @@ END;
export const INIT_PROPERTYNAME = `
CREATE TABLE IF NOT EXISTS propertyname (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -708,7 +711,7 @@ CREATE TABLE IF NOT EXISTS propertyname (
export const INIT_ARTICLEPROPERTY = `
CREATE TABLE IF NOT EXISTS articleproperty (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
property_id INTEGER NOT NULL,
value TEXT NOT NULL,
@@ -720,7 +723,7 @@ CREATE TABLE IF NOT EXISTS articleproperty (
export const INIT_ATTRIBUTENAME = `
CREATE TABLE IF NOT EXISTS attributename (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -732,7 +735,7 @@ CREATE TABLE IF NOT EXISTS attributename (
export const INIT_ATTRIBUTEVALUE = `
CREATE TABLE IF NOT EXISTS attributevalue (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
attribute_id INTEGER NOT NULL,
value TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -743,7 +746,7 @@ CREATE TABLE IF NOT EXISTS attributevalue (
export const INIT_ARTICLEATTRIBUTEVALUE = `
CREATE TABLE IF NOT EXISTS articleattributevalue (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
attributevalue_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -755,7 +758,7 @@ CREATE TABLE IF NOT EXISTS articleattributevalue (
export const INIT_CATEGORYPROPERTY = `
CREATE TABLE IF NOT EXISTS categoryproperty (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
property_id INTEGER NOT NULL,
value TEXT NOT NULL,
@@ -767,7 +770,7 @@ CREATE TABLE IF NOT EXISTS categoryproperty (
export const INIT_CATEGORYATTRIBUTEVALUE = `
CREATE TABLE IF NOT EXISTS categoryattributevalue (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
attributevalue_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
@@ -779,7 +782,7 @@ CREATE TABLE IF NOT EXISTS categoryattributevalue (
export const INIT_ACCOUNT = `
CREATE TABLE IF NOT EXISTS account (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
"type" TEXT NOT NULL,
"name" TEXT NOT NULL,
@@ -789,7 +792,7 @@ CREATE TABLE IF NOT EXISTS account (
export const INIT_PAYMENT = `
CREATE TABLE IF NOT EXISTS payment (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
manifest_id INTEGER NOT NULL,
"type" TEXT NOT NULL,
@@ -801,7 +804,7 @@ CREATE TABLE IF NOT EXISTS payment (
export const INIT_SHIPPING = `
CREATE TABLE IF NOT EXISTS shipping (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
manifest_id INTEGER NOT NULL,
"number" TEXT NOT NULL,
"type" TEXT NOT NULL,
@@ -812,7 +815,7 @@ CREATE TABLE IF NOT EXISTS shipping (
export const INIT_PACKAGE = `
CREATE TABLE IF NOT EXISTS package (
id INTEGER PRIMARY KEY,
id INTEGER PRIMARY KEY AUTOINCREMENT,
shipping_id INTEGER NOT NULL,
entityaddress_id INTEGER NOT NULL,
"number" TEXT NOT NULL,

View File

@@ -22,17 +22,17 @@ export default function addImage(provider) {
const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC;
/**
* Adds an image to an article by its SKU, processing it into AVIF variants.
* Adds an image to an article by its SKU, processing full, thumb, and 40px-edge small-thumb AVIF variants.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {Buffer} image - The source image buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, smallThumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
*
* @example
* const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, smallThumb: { w, h }, fullHash, thumbHash }
*/
return async (googleAuthId, sku, image, tx) => {
const runAdd = async (runner) => {
@@ -71,6 +71,9 @@ export default function addImage(provider) {
thumbw: processed.thumb.w,
thumbh: processed.thumb.h,
thumbhash: processed.thumb.hash,
smallthumbw: processed.smallThumb.w,
smallthumbh: processed.smallThumb.h,
smallthumbbinary: processed.smallThumb.buffer,
source: '',
fullbinary: processed.full.buffer,
thumbbinary: processed.thumb.buffer,
@@ -107,6 +110,10 @@ export default function addImage(provider) {
w: processed.thumb.w,
h: processed.thumb.h,
},
smallThumb: {
w: processed.smallThumb.w,
h: processed.smallThumb.h,
},
fullHash,
thumbHash: processed.thumb.hash,
};
@@ -121,4 +128,3 @@ export default function addImage(provider) {
return runAdd(makeRunner(provider));
};
}

View File

@@ -22,13 +22,13 @@ export default function addImage(provider) {
const addBrandPicSql = isSqlite ? SQLITE_ADD_BRANDPIC : MSSQL_ADD_BRANDPIC;
/**
* Adds an image to a brand, processing it into AVIF full and thumb variants.
* Adds an image to a brand, processing full, thumb, and 40px-edge small-thumb AVIF variants.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} brandId - The id of the brand.
* @param {Buffer} image - The source image buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array<object>}>}
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, pics: Array<object>}>}
*/
return async (googleAuthId, brandId, image, tx) => {
const runAdd = async (runner) => {
@@ -69,6 +69,9 @@ export default function addImage(provider) {
thumbw: processed.thumb.w,
thumbh: processed.thumb.h,
thumbhash: processed.thumb.hash,
smallthumbw: processed.smallThumb.w,
smallthumbh: processed.smallThumb.h,
smallthumbbinary: processed.smallThumb.buffer,
source: '',
fullbinary: processed.full.buffer,
thumbbinary: processed.thumb.buffer,
@@ -82,15 +85,14 @@ export default function addImage(provider) {
picId,
sort,
});
const brandPicId = brandPicResult.rows[0]?.id ?? brandPicResult.insertId;
const picObj = {
id: brandPicId,
sort,
fullHash,
thumbW: processed.thumb.w,
thumbH: processed.thumb.h,
thumbHash: processed.thumb.hash,
smallThumbW: processed.smallThumb.w,
smallThumbH: processed.smallThumb.h,
};
return {
@@ -99,6 +101,8 @@ export default function addImage(provider) {
thumbW: processed.thumb.w,
thumbH: processed.thumb.h,
thumbHash: processed.thumb.hash,
smallThumbW: processed.smallThumb.w,
smallThumbH: processed.smallThumb.h,
pics: [picObj],
};
};

78
src/store/brand/create.js Normal file
View File

@@ -0,0 +1,78 @@
import {
CREATE as MSSQL_CREATE,
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BY_COMPANY_AND_NAME,
} from '../../providers/mssql/queries/brand.js';
import {
CREATE as SQLITE_CREATE,
GET_BY_COMPANY_AND_NAME as SQLITE_GET_BY_COMPANY_AND_NAME,
} from '../../providers/sqlite3/queries/brand.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function create(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
const getSql = isSqlite
? SQLITE_GET_BY_COMPANY_AND_NAME
: MSSQL_GET_BY_COMPANY_AND_NAME;
/**
* Creates a brand for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - The brand input.
* @param {string} input.name - The brand name.
* @param {string} [input.gsprpinfo] - Optional GSPRP info.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The created brand.
* @throws {Error} If the brand is invalid or already exists.
*
* @example
* const brand = await store.brand.create(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
* // => { id, name, gsprpinfo, pics: [] }
*/
return async (googleAuthId, input = {}, tx) => {
const name = input.name?.trim();
if (!name) {
throw new Error('Invalid brand');
}
const gsprpinfo = input.gsprpinfo ?? null;
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, tx);
try {
const created = await runner.query(createSql, {
companyId,
name,
gsprpinfo,
});
const id = created.rows[0]?.id ?? created.insertId;
if (id == null) {
const fetched = await runner.query(getSql, { companyId, name });
const row = fetched.rows[0];
return {
id: row.id,
name,
gsprpinfo,
pics: [],
};
}
return {
id,
name,
gsprpinfo,
pics: [],
};
} catch (err) {
if (isUniqueConstraintError(err)) {
throw new Error('Brand already exists');
}
throw err;
}
};
}

View File

@@ -1,42 +1,102 @@
import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/brand.js';
import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/brand.js';
import {
COUNT_BY_COMPANY as MSSQL_COUNT,
LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY,
LIST_BY_COMPANY_PAGED as MSSQL_LIST_PAGED,
LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS as MSSQL_LIST_PAGED_WITH_SMALL_THUMBS,
} from '../../providers/mssql/queries/brand.js';
import {
COUNT_BY_COMPANY as SQLITE_COUNT,
LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY,
LIST_BY_COMPANY_PAGED as SQLITE_LIST_PAGED,
LIST_BY_COMPANY_PAGED_WITH_SMALL_THUMBS as SQLITE_LIST_PAGED_WITH_SMALL_THUMBS,
} from '../../providers/sqlite3/queries/brand.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function parsePics(raw) {
if (!raw) return [];
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
return Array.isArray(parsed) ? parsed : [];
if (!Array.isArray(parsed)) return [];
return parsed.map((pic) => {
const { smallThumbBinaryHex, ...result } = pic;
if (smallThumbBinaryHex === undefined) return result;
return {
...result,
smallThumbBinary: smallThumbBinaryHex === null
? null
: Buffer.from(smallThumbBinaryHex, 'hex').toString('base64'),
};
});
}
export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3'
? SQLITE_LIST_BY_COMPANY
: MSSQL_LIST_BY_COMPANY;
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY;
const sqlCount = isSqlite ? SQLITE_COUNT : MSSQL_COUNT;
const sqlPaged = isSqlite ? SQLITE_LIST_PAGED : MSSQL_LIST_PAGED;
const sqlPagedWithSmallThumbs = isSqlite
? SQLITE_LIST_PAGED_WITH_SMALL_THUMBS
: MSSQL_LIST_PAGED_WITH_SMALL_THUMBS;
/**
* Lists the brands of the user's selected company.
* Lists the brands of the user's selected company, sorted by name.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [opts] - Optional pagination options.
* @param {number} [opts.offset=0] - Number of brands to skip.
* @param {number} [opts.limit=10] - Number of brands to return (max 100).
* @param {boolean} [opts.includeSmallThumbs=false] - Include 40px-edge small thumbnail dimensions and base64 AVIF data.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>} The brands.
* @returns {Promise<{brands: Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW?: number|null, smallThumbH?: number|null, smallThumbBinary?: string|null}>}>, total: number, offset: number, limit: number}>} The brands plus the total unpaged count. Small-thumbnail fields are present only when `includeSmallThumbs` is true; the binary is base64-encoded AVIF data.
*
* @example
* const brands = await store.brand.list(42);
* // => [{ id, name, gsprpinfo, pics: [{ sort, fullHash, thumbW, thumbH, thumbHash }] }]
* const result = await store.brand.list(42, { offset: 0, limit: 10 });
* // => { brands: [{ id, name, gsprpinfo, pics: [...] }], total: 25, offset: 0, limit: 10 }
*/
return async (googleAuthId, tx) => {
const { companyId } = await access(googleAuthId, tx);
return async (googleAuthId, opts, tx) => {
let offset = 0;
let limit = 10;
let includeSmallThumbs = false;
let resolvedTx = tx;
const result = await makeRunner(provider, tx).query(sql, { companyId });
if (opts != null && typeof opts === 'object' && !opts._isTx) {
const parsedOffset = Number(opts.offset ?? 0);
const parsedLimit = Number(opts.limit ?? 10);
if (!Number.isInteger(parsedOffset) || parsedOffset < 0) {
throw new Error('Invalid offset');
}
if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) {
throw new Error('Invalid limit');
}
offset = parsedOffset;
limit = parsedLimit;
includeSmallThumbs = opts.includeSmallThumbs === true;
} else if (opts != null) {
resolvedTx = opts;
}
return result.rows.map((row) => ({
id: row.id,
name: row.name,
gsprpinfo: row.gsprpinfo,
pics: parsePics(row.picsJson),
}));
const runner = makeRunner(provider, resolvedTx);
const { companyId } = await access(googleAuthId, runner);
const [countResult, listResult] = await Promise.all([
runner.query(sqlCount, { companyId }),
runner.query(
includeSmallThumbs ? sqlPagedWithSmallThumbs : sqlPaged,
{ companyId, offset, limit },
),
]);
return {
brands: listResult.rows.map((row) => ({
id: row.id,
name: row.name,
gsprpinfo: row.gsprpinfo,
pics: parsePics(row.picsJson),
})),
total: Number(countResult.rows[0]?.total ?? 0),
offset,
limit,
};
};
}

90
src/store/brand/update.js Normal file
View File

@@ -0,0 +1,90 @@
import {
GET_BY_ID_AND_COMPANY as MSSQL_GET,
UPDATE_BY_ID as MSSQL_UPDATE,
} from '../../providers/mssql/queries/brand.js';
import {
GET_BY_ID_AND_COMPANY as SQLITE_GET,
UPDATE_BY_ID as SQLITE_UPDATE,
} from '../../providers/sqlite3/queries/brand.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function update(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getSql = isSqlite ? SQLITE_GET : MSSQL_GET;
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
/**
* Renames/updates a brand for the user's selected company by id.
* Touching `article.updated_at` for all articles linked via `brand_id`
* when the name actually changes, so list caches invalidate.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The id of the brand to update.
* @param {object} input - The update input.
* @param {string} [input.name] - The new brand name.
* @param {string} [input.gsprpinfo] - The new GSPRP info.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null, touchedArticleCount: number}>} The updated brand plus the number of linked articles whose `updated_at` was bumped.
* @throws {Error} If the brand is not found, invalid, or the name is already taken.
*
* @example
* const brand = await store.brand.update(42, 3, { name: 'Coca-Cola GmbH' });
* // => { id: 3, name, gsprpinfo, touchedArticleCount: 2 }
*/
return async (googleAuthId, id, input = {}, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Brand not found');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, tx);
const existing = await runner.query(getSql, { id: numericId, companyId });
const current = existing.rows[0];
if (!current) {
throw new Error('Brand not found');
}
const name = input.name !== undefined ? input.name?.trim() : current.name;
if (!name) {
throw new Error('Invalid brand');
}
const gsprpinfo = input.gsprpinfo !== undefined ? input.gsprpinfo : current.gsprpinfo;
try {
await runner.query(updateSql, {
id: numericId,
companyId,
name,
gsprpinfo,
});
} catch (err) {
if (isUniqueConstraintError(err)) {
throw new Error('Brand already exists');
}
throw err;
}
let touchedArticleCount = 0;
if (name !== current.name) {
// Brand name is denormalized into article list views — bump
// updated_at so caches/incremental syncs pick up the rename.
const touchSql = isSqlite
? "UPDATE article SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE brand_id = @brandId AND company_id = @companyId"
: 'UPDATE article SET updated_at = SYSUTCDATETIME() WHERE brand_id = @brandId AND company_id = @companyId';
const touched = await runner.query(touchSql, { brandId: numericId, companyId });
touchedArticleCount = touched.rowsAffected ?? 0;
}
return {
id: numericId,
name,
gsprpinfo,
touchedArticleCount,
};
};
}

View File

@@ -34,7 +34,7 @@ export default function upsert(provider) {
* @param {string} input.name - The brand name.
* @param {string} [input.gsprpinfo] - Optional GSPRP info.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The upserted brand.
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>} The upserted brand.
* @throws {Error} If the brand is invalid.
*
* @example

View File

@@ -22,13 +22,13 @@ export default function addImage(provider) {
const addCategoryPicSql = isSqlite ? SQLITE_ADD_CATEGORYPIC : MSSQL_ADD_CATEGORYPIC;
/**
* Adds an image to a category, processing it into AVIF full and thumb variants.
* Adds an image to a category, processing full, thumb, and 40px-edge small-thumb AVIF variants.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The id of the category.
* @param {Buffer} image - The source image buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info.
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number}>} The added image info.
*
* @example
* const result = await store.category.addImage(42, 5, imageBuffer);
@@ -71,6 +71,9 @@ export default function addImage(provider) {
thumbw: processed.thumb.w,
thumbh: processed.thumb.h,
thumbhash: processed.thumb.hash,
smallthumbw: processed.smallThumb.w,
smallthumbh: processed.smallThumb.h,
smallthumbbinary: processed.smallThumb.buffer,
source: '',
fullbinary: processed.full.buffer,
thumbbinary: processed.thumb.buffer,
@@ -98,6 +101,8 @@ export default function addImage(provider) {
thumbW: processed.thumb.w,
thumbH: processed.thumb.h,
thumbHash: processed.thumb.hash,
smallThumbW: processed.smallThumb.w,
smallThumbH: processed.smallThumb.h,
};
};

View File

@@ -3,6 +3,7 @@ import { rgbaToThumbHash } from 'thumbhash';
const FULL_MAX = 1000;
const THUMB_MAX = 200;
const SMALL_THUMB_MAX = 40;
export default async function processAvifImage(inputBuffer) {
const input = Buffer.isBuffer(inputBuffer)
@@ -26,8 +27,10 @@ export default async function processAvifImage(inputBuffer) {
const thumbPipeline = sharp(input)
.resize(THUMB_MAX, THUMB_MAX, { fit: 'inside', withoutEnlargement: true });
const smallThumbPipeline = sharp(input)
.resize(SMALL_THUMB_MAX, SMALL_THUMB_MAX, { fit: 'inside', withoutEnlargement: true });
const [full, thumb, thumbRgba] = await Promise.all([
const [full, thumb, thumbRgba, smallThumb] = await Promise.all([
useOriginalFull
? Promise.resolve({
info: { width: w, height: h },
@@ -44,6 +47,9 @@ export default async function processAvifImage(inputBuffer) {
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true }),
smallThumbPipeline
.avif({ effort: 2 })
.toBuffer({ resolveWithObject: true }),
]);
const thumbHashBytes = rgbaToThumbHash(
@@ -66,5 +72,10 @@ export default async function processAvifImage(inputBuffer) {
buffer: thumb.data,
hash: Buffer.from(thumbHashBytes).toString('base64'),
},
smallThumb: {
w: smallThumb.info.width,
h: smallThumb.info.height,
buffer: smallThumb.data,
},
};
}