This commit is contained in:
seb
2026-09-29 17:29:13 +02:00
parent ae5d5cc6f8
commit 30820ba8c7
14 changed files with 69 additions and 36 deletions

View File

@@ -37,13 +37,13 @@ Lists the properties of an article by its SKU.
**Returns** **Returns**
`Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>` `Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
## `articleproperty.setByArticleSku` ## `articleproperty.setByArticleSku`
*Source: `src/store/articleproperty/setByArticleSku.js`* *Source: `src/store/articleproperty/setByArticleSku.js`*
Replaces all properties for an article by its SKU. Replaces all properties for an article by its SKU. Enforces exclusive flag on property names.
**Parameters** **Parameters**
@@ -56,4 +56,4 @@ Replaces all properties for an article by its SKU.
**Returns** **Returns**
`Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>` `Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`

10
API.md
View File

@@ -222,8 +222,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `setByArticleSku(googleAuthId: number, sku: string, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>` - `setByArticleSku(googleAuthId: number, sku: string, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
- [articleproperty](API.articleproperty.md) - [articleproperty](API.articleproperty.md)
- `delete(googleAuthId: number, sku: string, propertyId: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, sku: string, propertyId: number, tx?: object): Promise<void>`
- `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>` - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
- `setByArticleSku(googleAuthId: number, sku: string, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>` - `setByArticleSku(googleAuthId: number, sku: string, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
- [attributename](API.attributename.md) - [attributename](API.attributename.md)
- `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>` - `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
@@ -243,10 +243,10 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
- `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
- [propertyname](API.propertyname.md) - [propertyname](API.propertyname.md)
- `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, exclusive: boolean}>>`
- `order(googleAuthId: number, input: {propertynameIds: Array<number>}, tx?: object): Promise<{propertynameIds: Array<number>}>` - `order(googleAuthId: number, input: {propertynameIds: Array<number>}, tx?: object): Promise<{propertynameIds: Array<number>}>`
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
- [runner.js](API.runner.js.md) - [runner.js](API.runner.js.md)
- `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }` - `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`

View File

@@ -16,11 +16,12 @@ Creates a property name for the user's selected company.
| `input` | `object` | The property name input. | | `input` | `object` | The property name input. |
| `input.name` | `string` | The property name. | | `input.name` | `string` | The property name. |
| `[input.sort]` | `number` | Optional sort order. | | `[input.sort]` | `number` | Optional sort order. |
| `[input.exclusive=false]` | `boolean` | Whether values are mutually exclusive on an article. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**
`Promise<{id: number, name: string, sort: number}>` `Promise<{id: number, name: string, sort: number, exclusive: boolean}>`
## `propertyname.delete` ## `propertyname.delete`
@@ -55,7 +56,7 @@ Lists the property names for the user's selected company.
**Returns** **Returns**
`Promise<Array<{id: number, name: string, sort: number}>>` `Promise<Array<{id: number, name: string, sort: number, exclusive: boolean}>>`
## `propertyname.order` ## `propertyname.order`
@@ -91,8 +92,9 @@ Updates a property name for the user's selected company.
| `input` | `object` | Update patch. | | `input` | `object` | Update patch. |
| `[input.name]` | `string` | New name. | | `[input.name]` | `string` | New name. |
| `[input.sort]` | `number` | New sort. | | `[input.sort]` | `number` | New sort. |
| `[input.exclusive]` | `boolean` | New exclusive flag. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**
`Promise<{id: number, name: string, sort: number}>` `Promise<{id: number, name: string, sort: number, exclusive: boolean}>`

View File

@@ -177,6 +177,11 @@ export class MssqlProvider {
BEGIN BEGIN
ALTER TABLE posterminal ADD clientserial NVARCHAR(255) NULL; ALTER TABLE posterminal ADD clientserial NVARCHAR(255) NULL;
END END
IF OBJECT_ID(N'dbo.propertyname', N'U') IS NOT NULL AND COL_LENGTH('propertyname', 'exclusive') IS NULL
BEGIN
ALTER TABLE propertyname ADD exclusive BIT NOT NULL CONSTRAINT DF_propertyname_exclusive DEFAULT 0;
END
`); `);
} catch { } catch {
// ignore // ignore

View File

@@ -4,6 +4,7 @@ SELECT
ap.article_id AS articleId, ap.article_id AS articleId,
ap.property_id AS propertyId, ap.property_id AS propertyId,
pn.name AS name, pn.name AS name,
pn.exclusive,
ap.value, ap.value,
ap.sort ap.sort
FROM articleproperty ap FROM articleproperty ap

View File

@@ -2,7 +2,8 @@ export const LIST_BY_COMPANY = `
SELECT SELECT
id, id,
name, name,
sort sort,
exclusive
FROM propertyname FROM propertyname
WHERE company_id = @companyId WHERE company_id = @companyId
ORDER BY sort ASC, id ASC ORDER BY sort ASC, id ASC
@@ -20,7 +21,8 @@ SELECT
id, id,
company_id, company_id,
name, name,
sort sort,
exclusive
FROM propertyname FROM propertyname
WHERE id = @id AND company_id = @companyId WHERE id = @id AND company_id = @companyId
`; `;
@@ -30,7 +32,8 @@ SELECT
id, id,
company_id, company_id,
name, name,
sort sort,
exclusive
FROM propertyname FROM propertyname
WHERE name = @name AND company_id = @companyId WHERE name = @name AND company_id = @companyId
`; `;
@@ -42,14 +45,14 @@ WHERE company_id = @companyId
`; `;
export const CREATE = ` export const CREATE = `
INSERT INTO propertyname (company_id, name, sort) INSERT INTO propertyname (company_id, name, sort, exclusive)
OUTPUT INSERTED.id, INSERTED.name, INSERTED.sort OUTPUT INSERTED.id, INSERTED.name, INSERTED.sort, INSERTED.exclusive
VALUES (@companyId, @name, @sort) VALUES (@companyId, @name, @sort, @exclusive)
`; `;
export const UPDATE_BY_ID = ` export const UPDATE_BY_ID = `
UPDATE propertyname UPDATE propertyname
SET name = @name, sort = @sort SET name = @name, sort = @sort, exclusive = @exclusive
WHERE id = @id AND company_id = @companyId WHERE id = @id AND company_id = @companyId
`; `;

View File

@@ -902,6 +902,7 @@ BEGIN
company_id INT NOT NULL, company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL, name NVARCHAR(255) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_propertyname_sort DEFAULT 0, sort INT NOT NULL CONSTRAINT DF_propertyname_sort DEFAULT 0,
exclusive BIT NOT NULL CONSTRAINT DF_propertyname_exclusive DEFAULT 0,
CONSTRAINT FK_propertyname_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, CONSTRAINT FK_propertyname_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT UQ_propertyname_company_name UNIQUE (company_id, name) CONSTRAINT UQ_propertyname_company_name UNIQUE (company_id, name)
); );
@@ -918,8 +919,7 @@ BEGIN
value NVARCHAR(MAX) NOT NULL, value NVARCHAR(MAX) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_articleproperty_sort DEFAULT 0, sort INT NOT NULL CONSTRAINT DF_articleproperty_sort DEFAULT 0,
CONSTRAINT FK_articleproperty_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, CONSTRAINT FK_articleproperty_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
CONSTRAINT FK_articleproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE, CONSTRAINT FK_articleproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
CONSTRAINT UQ_articleproperty_article_property UNIQUE (article_id, property_id)
); );
END END
`; `;

View File

@@ -421,6 +421,11 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
if (posterminalCols.length > 0 && !posterminalCols.includes('clientserial')) { if (posterminalCols.length > 0 && !posterminalCols.includes('clientserial')) {
this.db.exec(`ALTER TABLE posterminal ADD COLUMN clientserial TEXT`); this.db.exec(`ALTER TABLE posterminal ADD COLUMN clientserial TEXT`);
} }
const propertynameCols = this.db.pragma('table_info("propertyname")').map((c) => c.name);
if (propertynameCols.length > 0 && !propertynameCols.includes('exclusive')) {
this.db.exec(`ALTER TABLE propertyname ADD COLUMN exclusive INTEGER NOT NULL DEFAULT 0`);
}
} catch { } catch {
// ignore // ignore
} }

View File

@@ -627,6 +627,7 @@ CREATE TABLE IF NOT EXISTS propertyname (
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0, sort INTEGER NOT NULL DEFAULT 0,
exclusive INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
UNIQUE (company_id, name) UNIQUE (company_id, name)
); );
@@ -640,8 +641,7 @@ CREATE TABLE IF NOT EXISTS articleproperty (
value TEXT NOT NULL, value TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0, sort INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE, FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
UNIQUE (article_id, property_id)
); );
`; `;

View File

@@ -13,7 +13,7 @@ export default function listByArticleSku(provider) {
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU. * @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>} * @returns {Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
*/ */
return async (googleAuthId, sku, tx) => { return async (googleAuthId, sku, tx) => {
const runner = makeRunner(provider, tx); const runner = makeRunner(provider, tx);
@@ -25,6 +25,7 @@ export default function listByArticleSku(provider) {
articleId: row.articleId, articleId: row.articleId,
propertyId: row.propertyId, propertyId: row.propertyId,
name: row.name, name: row.name,
exclusive: Boolean(row.exclusive),
value: row.value, value: row.value,
sort: row.sort, sort: row.sort,
})); }));

View File

@@ -24,12 +24,13 @@ export default function setByArticleSku(provider) {
/** /**
* Replaces all properties for an article by its SKU. * Replaces all properties for an article by its SKU.
* Enforces exclusive flag on property names.
* *
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU. * @param {string} sku - The article SKU.
* @param {Array<{propertyId: number, value: string, sort?: number}>} properties - The properties to set. * @param {Array<{propertyId: number, value: string, sort?: number}>} properties - The properties to set.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, articleId: number, propertyId: number, name: string, value: string, sort: number}>>} * @returns {Promise<Array<{id: number, articleId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
*/ */
return async (googleAuthId, sku, properties = [], tx) => { return async (googleAuthId, sku, properties = [], tx) => {
if (!Array.isArray(properties)) { if (!Array.isArray(properties)) {
@@ -40,7 +41,7 @@ export default function setByArticleSku(provider) {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner); const { articleId, companyId } = await resolve(googleAuthId, sku, runner);
const validated = []; const validated = [];
const seenProps = new Set(); const seenExclusiveProps = new Set();
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const item = properties[i]; const item = properties[i];
@@ -48,21 +49,27 @@ export default function setByArticleSku(provider) {
if (!Number.isInteger(propertyId) || propertyId <= 0) { if (!Number.isInteger(propertyId) || propertyId <= 0) {
throw new Error('Invalid propertyId'); throw new Error('Invalid propertyId');
} }
if (seenProps.has(propertyId)) {
throw new Error('Duplicate propertyId');
}
seenProps.add(propertyId);
const propRes = await runner.query(getPropSql, { id: propertyId, companyId }); const propRes = await runner.query(getPropSql, { id: propertyId, companyId });
if (!propRes.rows[0]) { const prop = propRes.rows[0];
if (!prop) {
throw new Error(`Property not found: ${propertyId}`); throw new Error(`Property not found: ${propertyId}`);
} }
const isExclusive = Boolean(prop.exclusive);
if (isExclusive) {
if (seenExclusiveProps.has(propertyId)) {
throw new Error(`Property "${prop.name}" is exclusive and can only have one value`);
}
seenExclusiveProps.add(propertyId);
}
const value = item.value != null ? String(item.value).trim() : ''; const value = item.value != null ? String(item.value).trim() : '';
const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i; const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i;
validated.push({ validated.push({
propertyId, propertyId,
name: propRes.rows[0].name, name: prop.name,
exclusive: isExclusive,
value, value,
sort, sort,
}); });
@@ -84,6 +91,7 @@ export default function setByArticleSku(provider) {
articleId, articleId,
propertyId: item.propertyId, propertyId: item.propertyId,
name: item.name, name: item.name,
exclusive: item.exclusive,
value: item.value, value: item.value,
sort: item.sort, sort: item.sort,
}); });

View File

@@ -22,8 +22,9 @@ export default function create(provider) {
* @param {object} input - The property name input. * @param {object} input - The property name input.
* @param {string} input.name - The property name. * @param {string} input.name - The property name.
* @param {number} [input.sort] - Optional sort order. * @param {number} [input.sort] - Optional sort order.
* @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
* @returns {Promise<{id: number, name: string, sort: number}>} * @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
*/ */
return async (googleAuthId, input = {}, tx) => { return async (googleAuthId, input = {}, tx) => {
const name = input.name?.trim(); const name = input.name?.trim();
@@ -40,13 +41,16 @@ export default function create(provider) {
sort = next.rows[0].sort; sort = next.rows[0].sort;
} }
const exclusive = Boolean(input.exclusive) ? 1 : 0;
const result = await runner.query(createSql, { const result = await runner.query(createSql, {
companyId, companyId,
name, name,
sort, sort,
exclusive,
}); });
const id = result.rows[0]?.id ?? result.insertId; const id = result.rows[0]?.id ?? result.insertId;
return { id, name, sort }; return { id, name, sort, exclusive: Boolean(exclusive) };
}; };
} }

View File

@@ -12,7 +12,7 @@ export default function list(provider) {
* *
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, name: string, sort: number}>>} * @returns {Promise<Array<{id: number, name: string, sort: number, exclusive: boolean}>>}
*/ */
return async (googleAuthId, tx) => { return async (googleAuthId, tx) => {
const { companyId } = await access(googleAuthId, tx); const { companyId } = await access(googleAuthId, tx);
@@ -21,6 +21,7 @@ export default function list(provider) {
id: row.id, id: row.id,
name: row.name, name: row.name,
sort: row.sort, sort: row.sort,
exclusive: Boolean(row.exclusive),
})); }));
}; };
} }

View File

@@ -23,8 +23,9 @@ export default function update(provider) {
* @param {object} input - Update patch. * @param {object} input - Update patch.
* @param {string} [input.name] - New name. * @param {string} [input.name] - New name.
* @param {number} [input.sort] - New sort. * @param {number} [input.sort] - New sort.
* @param {boolean} [input.exclusive] - New exclusive flag.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
* @returns {Promise<{id: number, name: string, sort: number}>} * @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean}>}
*/ */
return async (googleAuthId, id, input = {}, tx) => { return async (googleAuthId, id, input = {}, tx) => {
if (!id || !Number.isInteger(Number(id))) { if (!id || !Number.isInteger(Number(id))) {
@@ -44,14 +45,16 @@ export default function update(provider) {
throw new Error('Name required'); throw new Error('Name required');
} }
const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort; const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort;
const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive;
await runner.query(updateSql, { await runner.query(updateSql, {
id, id,
companyId, companyId,
name, name,
sort, sort,
exclusive,
}); });
return { id, name, sort }; return { id, name, sort, exclusive: Boolean(exclusive) };
}; };
} }