This commit is contained in:
seb
2026-09-27 22:01:56 +02:00
parent 0cafa11a32
commit 87da854de4
12 changed files with 686 additions and 0 deletions

146
API.labeltemplate.md Normal file
View File

@@ -0,0 +1,146 @@
# labeltemplate
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `labeltemplate.create`
*Source: `src/store/labeltemplate/create.js`*
Creates a new label template for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Label template input. |
| `input.name` | `string` | Template name. |
| `input.width` | `number` | Canvas width in pixels. |
| `input.height` | `number` | Canvas height in pixels. |
| `input.dpi` | `number` | Printer DPI (203 or 300). |
| `input.type` | `string` | Template type (`"zpl"`). |
| `input.content` | `object` | Template content (JSON — label editor objects). |
| `[input.thumb]` | `Buffer\|null` | Optional pre-rendered 50×50 1-bit PNG thumbnail. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
**Throws**
`Error` — If name, content or dimensions are missing, or dpi/type is invalid.
**Example**
```js
const template = await store.labeltemplate.create(googleAuthId, {
name: 'Product Label',
width: 448,
height: 200,
dpi: 203,
type: 'zpl',
content: { objects: [] },
});
```
## `labeltemplate.delete`
*Source: `src/store/labeltemplate/delete.js`*
Deletes a label template for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The label template ID to delete. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — If template is not found.
## `labeltemplate.getById`
*Source: `src/store/labeltemplate/getById.js`*
Retrieves a label template by its ID for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The label template ID. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>`
**Example**
```js
const template = await store.labeltemplate.getById(googleAuthId, 1);
// => { id: 1, company_id: 1, name: 'Product Label', width: 448, height: 200, dpi: 203, type: 'zpl', ... }
```
## `labeltemplate.list`
*Source: `src/store/labeltemplate/list.js`*
Lists all label templates for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
**Example**
```js
const templates = await store.labeltemplate.list(42);
// => [{ id: 1, company_id: 1, name: 'Product Label', ... }]
```
## `labeltemplate.update`
*Source: `src/store/labeltemplate/update.js`*
Updates a label template for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The label template ID to update. |
| `input` | `object` | Update payload (all fields optional). |
| `[input.name]` | `string` | Template name. |
| `[input.width]` | `number` | Canvas width in pixels. |
| `[input.height]` | `number` | Canvas height in pixels. |
| `[input.dpi]` | `number` | Printer DPI (203 or 300). |
| `[input.type]` | `string` | Template type (`"zpl"`). |
| `[input.content]` | `object` | Template content (JSON). |
| `[input.thumb]` | `Buffer\|null` | 50×50 1-bit PNG thumbnail. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
**Throws**
`Error` — If template not found, name is empty, or dpi/type is invalid.

View File

@@ -139,6 +139,11 @@ import printerList from './store/printer/list.js';
import printerUpdate from './store/printer/update.js'; import printerUpdate from './store/printer/update.js';
import printerDelete from './store/printer/delete.js'; import printerDelete from './store/printer/delete.js';
import printerOrder from './store/printer/order.js'; import printerOrder from './store/printer/order.js';
import labeltemplateCreate from './store/labeltemplate/create.js';
import labeltemplateGetById from './store/labeltemplate/getById.js';
import labeltemplateList from './store/labeltemplate/list.js';
import labeltemplateUpdate from './store/labeltemplate/update.js';
import labeltemplateDelete from './store/labeltemplate/delete.js';
import stockAdjust from './store/stock/adjust.js'; import stockAdjust from './store/stock/adjust.js';
export default class LibDashStore { export default class LibDashStore {
@@ -377,6 +382,14 @@ export default class LibDashStore {
order: printerOrder(provider), order: printerOrder(provider),
}; };
this.labeltemplate = {
create: labeltemplateCreate(provider),
getById: labeltemplateGetById(provider),
list: labeltemplateList(provider),
update: labeltemplateUpdate(provider),
delete: labeltemplateDelete(provider),
};
this.stock = { this.stock = {
adjust: stockAdjust(provider), adjust: stockAdjust(provider),
}; };

View File

@@ -1,6 +1,7 @@
/** Drop order = reverse of schema create dependency order. */ /** Drop order = reverse of schema create dependency order. */
export const TABLES = [ export const TABLES = [
'manifestattachment', 'manifestattachment',
'labeltemplate',
'file', 'file',
'companyuser', 'companyuser',
'syncpos', 'syncpos',

View File

@@ -0,0 +1,87 @@
export const LABELTEMPLATE_COLUMNS = `
id,
company_id,
name,
width,
height,
dpi,
type,
content,
thumb
`;
export const LIST_BY_COMPANY = `
SELECT
${LABELTEMPLATE_COLUMNS}
FROM labeltemplate
WHERE company_id = @companyId
ORDER BY id ASC
`;
export const GET_BY_COMPANY_AND_ID = `
SELECT
${LABELTEMPLATE_COLUMNS}
FROM labeltemplate
WHERE id = @id AND company_id = @companyId
`;
export const CREATE = `
INSERT INTO labeltemplate (
company_id,
name,
width,
height,
dpi,
type,
content,
thumb
)
OUTPUT
INSERTED.id,
INSERTED.company_id,
INSERTED.name,
INSERTED.width,
INSERTED.height,
INSERTED.dpi,
INSERTED.type,
INSERTED.content,
INSERTED.thumb
VALUES (
@companyId,
@name,
@width,
@height,
@dpi,
@type,
@content,
@thumb
)
`;
export const UPDATE = `
UPDATE labeltemplate
SET
name = @name,
width = @width,
height = @height,
dpi = @dpi,
type = @type,
content = @content,
thumb = @thumb
OUTPUT
INSERTED.id,
INSERTED.company_id,
INSERTED.name,
INSERTED.width,
INSERTED.height,
INSERTED.dpi,
INSERTED.type,
INSERTED.content,
INSERTED.thumb
WHERE id = @id AND company_id = @companyId
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM labeltemplate
WHERE id = @id AND company_id = @companyId
`;

View File

@@ -816,6 +816,26 @@ END
`; `;
export const INIT_LABELTEMPLATE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'labeltemplate')
BEGIN
CREATE TABLE labeltemplate (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
width INT NOT NULL,
height INT NOT NULL,
dpi INT NOT NULL,
type NVARCHAR(50) NOT NULL,
content NVARCHAR(MAX) NOT NULL,
thumb VARBINARY(MAX) NULL,
CONSTRAINT FK_labeltemplate_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT CK_labeltemplate_dpi CHECK (dpi IN (203, 300)),
CONSTRAINT CK_labeltemplate_type CHECK (type IN ('zpl'))
);
END
`;
export const INIT_FILE = ` export const INIT_FILE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file')
BEGIN BEGIN
@@ -919,6 +939,7 @@ export const SCHEMA_SCRIPTS = [
INIT_POSTERMINAL, INIT_POSTERMINAL,
INIT_POSTERMINALCATEGORY, INIT_POSTERMINALCATEGORY,
INIT_PRINTER, INIT_PRINTER,
INIT_LABELTEMPLATE,
INIT_FILE, INIT_FILE,
INIT_MANIFESTATTACHMENT, INIT_MANIFESTATTACHMENT,
INIT_MANIFESTATTACHMENT_TRIGGER, INIT_MANIFESTATTACHMENT_TRIGGER,

View File

@@ -0,0 +1,12 @@
import * as source from '../../mssql/queries/labeltemplate.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'labeltemplate');
export const {
CREATE,
DELETE_BY_COMPANY_AND_ID,
GET_BY_COMPANY_AND_ID,
LABELTEMPLATE_COLUMNS,
LIST_BY_COMPANY,
UPDATE,
} = queries;

View File

@@ -565,6 +565,21 @@ CREATE TABLE IF NOT EXISTS printer (
`; `;
export const INIT_LABELTEMPLATE = `
CREATE TABLE IF NOT EXISTS labeltemplate (
id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
dpi INTEGER NOT NULL CHECK (dpi IN (203, 300)),
type TEXT NOT NULL CHECK (type IN ('zpl')),
content TEXT NOT NULL,
thumb BLOB,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
`;
export const INIT_FILE = ` export const INIT_FILE = `
CREATE TABLE IF NOT EXISTS "file" ( CREATE TABLE IF NOT EXISTS "file" (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@@ -648,6 +663,7 @@ export const SCHEMA_SCRIPTS = [
INIT_POSTERMINAL, INIT_POSTERMINAL,
INIT_POSTERMINALCATEGORY, INIT_POSTERMINALCATEGORY,
INIT_PRINTER, INIT_PRINTER,
INIT_LABELTEMPLATE,
INIT_FILE, INIT_FILE,
INIT_MANIFESTATTACHMENT, INIT_MANIFESTATTACHMENT,
INIT_MANIFESTATTACHMENT_TRIGGER, INIT_MANIFESTATTACHMENT_TRIGGER,

View File

@@ -0,0 +1,121 @@
import {
CREATE as MSSQL_CREATE,
} from '../../providers/mssql/queries/labeltemplate.js';
import {
CREATE as SQLITE_CREATE,
} from '../../providers/sqlite3/queries/labeltemplate.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
const VALID_TYPES = new Set(['zpl']);
const VALID_DPIS = new Set([203, 300]);
function shape(row) {
return {
id: row.id,
company_id: row.company_id ?? row.companyId,
name: row.name,
width: row.width,
height: row.height,
dpi: row.dpi,
type: row.type,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
thumb: row.thumb ?? null,
};
}
export default function create(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
/**
* Creates a new label template for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Label template input.
* @param {string} input.name - Template name.
* @param {number} input.width - Canvas width in pixels.
* @param {number} input.height - Canvas height in pixels.
* @param {number} input.dpi - Printer DPI (203 or 300).
* @param {string} input.type - Template type ("zpl").
* @param {object} input.content - Template content (JSON).
* @param {Buffer|null} [input.thumb] - 50×50 1-bit PNG thumbnail buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>}
* @throws {Error} If required fields are missing or invalid.
*
* @example
* const template = await store.labeltemplate.create(googleAuthId, {
* name: 'Product Label',
* width: 448,
* height: 200,
* dpi: 203,
* type: 'zpl',
* content: { objects: [] },
* });
*/
return async (googleAuthId, input = {}, tx) => {
const name = typeof input.name === 'string' ? input.name.trim() : '';
if (!name) {
throw new Error('Name required');
}
const width = Number(input.width);
if (!Number.isInteger(width) || width <= 0) {
throw new Error('Invalid width');
}
const height = Number(input.height);
if (!Number.isInteger(height) || height <= 0) {
throw new Error('Invalid height');
}
const dpi = Number(input.dpi);
if (!VALID_DPIS.has(dpi)) {
throw new Error('Invalid dpi: must be 203 or 300');
}
const type = typeof input.type === 'string' ? input.type.trim() : '';
if (!type || !VALID_TYPES.has(type)) {
throw new Error('Invalid type: must be "zpl"');
}
if (input.content == null) {
throw new Error('Content required');
}
const content = typeof input.content === 'string'
? input.content
: JSON.stringify(input.content);
const thumb = input.thumb instanceof Buffer ? input.thumb : null;
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(createSql, {
companyId,
name,
width,
height,
dpi,
type,
content,
thumb,
});
const row = result.rows[0] ?? {
id: result.insertId,
company_id: companyId,
name,
width,
height,
dpi,
type,
content,
thumb,
};
return shape(row);
};
}

View File

@@ -0,0 +1,35 @@
import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/labeltemplate.js';
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/labeltemplate.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
/**
* Deletes a label template for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The label template ID to delete.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<void>}
* @throws {Error} If template is not found.
*/
return async (googleAuthId, id, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Label template not found');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(sql, { id: numericId, companyId });
if (!result.rowsAffected) {
throw new Error('Label template not found');
}
};
}

View File

@@ -0,0 +1,49 @@
import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/labeltemplate.js';
import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/labeltemplate.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
company_id: row.company_id ?? row.companyId,
name: row.name,
width: row.width,
height: row.height,
dpi: row.dpi,
type: row.type,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
thumb: row.thumb ?? null,
};
}
export default function getById(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
/**
* Retrieves a label template by its ID for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The label template ID.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>}
*
* @example
* const template = await store.labeltemplate.getById(googleAuthId, 1);
*/
return async (googleAuthId, id, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
return undefined;
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(sql, { id: numericId, companyId });
const row = result.rows[0];
return row ? shape(row) : undefined;
};
}

View File

@@ -0,0 +1,44 @@
import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/labeltemplate.js';
import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/labeltemplate.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
company_id: row.company_id ?? row.companyId,
name: row.name,
width: row.width,
height: row.height,
dpi: row.dpi,
type: row.type,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
thumb: row.thumb ?? null,
};
}
export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3'
? SQLITE_LIST_BY_COMPANY
: MSSQL_LIST_BY_COMPANY;
/**
* Lists all label templates for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>}
*
* @example
* const templates = await store.labeltemplate.list(42);
*/
return async (googleAuthId, tx) => {
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(sql, { companyId });
return result.rows.map(shape);
};
}

View File

@@ -0,0 +1,141 @@
import {
GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID,
UPDATE as MSSQL_UPDATE,
} from '../../providers/mssql/queries/labeltemplate.js';
import {
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
UPDATE as SQLITE_UPDATE,
} from '../../providers/sqlite3/queries/labeltemplate.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
const VALID_TYPES = new Set(['zpl']);
const VALID_DPIS = new Set([203, 300]);
function shape(row) {
return {
id: row.id,
company_id: row.company_id ?? row.companyId,
name: row.name,
width: row.width,
height: row.height,
dpi: row.dpi,
type: row.type,
content: typeof row.content === 'string' ? JSON.parse(row.content) : row.content,
thumb: row.thumb ?? null,
};
}
export default function update(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
/**
* Updates a label template for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The label template ID to update.
* @param {object} input - Update payload.
* @param {string} [input.name] - Template name.
* @param {number} [input.width] - Canvas width in pixels.
* @param {number} [input.height] - Canvas height in pixels.
* @param {number} [input.dpi] - Printer DPI (203 or 300).
* @param {string} [input.type] - Template type ("zpl").
* @param {object} [input.content] - Template content (JSON).
* @param {Buffer|null} [input.thumb] - 50×50 1-bit PNG thumbnail buffer.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>}
* @throws {Error} If template not found or input invalid.
*/
return async (googleAuthId, id, input = {}, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Label template not found');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const existing = await runner.query(getSql, { id: numericId, companyId });
const current = existing.rows[0];
if (!current) {
throw new Error('Label template not found');
}
let name = current.name;
if (input.name !== undefined) {
const cleaned = typeof input.name === 'string' ? input.name.trim() : '';
if (!cleaned) throw new Error('Name required');
name = cleaned;
}
let width = current.width;
if (input.width !== undefined) {
const w = Number(input.width);
if (!Number.isInteger(w) || w <= 0) throw new Error('Invalid width');
width = w;
}
let height = current.height;
if (input.height !== undefined) {
const h = Number(input.height);
if (!Number.isInteger(h) || h <= 0) throw new Error('Invalid height');
height = h;
}
let dpi = current.dpi;
if (input.dpi !== undefined) {
const d = Number(input.dpi);
if (!VALID_DPIS.has(d)) throw new Error('Invalid dpi: must be 203 or 300');
dpi = d;
}
let type = current.type;
if (input.type !== undefined) {
const cleaned = typeof input.type === 'string' ? input.type.trim() : '';
if (!cleaned || !VALID_TYPES.has(cleaned)) throw new Error('Invalid type: must be "zpl"');
type = cleaned;
}
let content = current.content;
if (input.content !== undefined) {
if (input.content == null) throw new Error('Content required');
content = typeof input.content === 'string'
? input.content
: JSON.stringify(input.content);
}
let thumb = current.thumb ?? null;
if (input.thumb !== undefined) {
thumb = input.thumb instanceof Buffer ? input.thumb : null;
}
const result = await runner.query(updateSql, {
id: numericId,
companyId,
name,
width,
height,
dpi,
type,
content,
thumb,
});
const row = result.rows[0] ?? {
id: numericId,
company_id: companyId,
name,
width,
height,
dpi,
type,
content,
thumb,
};
return shape(row);
};
}