From f612c87b6ee75b0b6fe6764b21d42abdcfbae74e Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 31 Oct 2026 20:28:39 +0100 Subject: [PATCH] u --- API.agentprinter.md | 119 ++++++++++++++++++ API.md | 7 ++ scripts/test-printeragent.js | 58 +++++++-- src/index.js | 15 +++ src/providers/mssql/downgrade/4_to_3.js | 38 ++++++ src/providers/mssql/purgeSql.js | 1 + src/providers/mssql/queries/agentprinter.js | 69 ++++++++++ src/providers/mssql/queries/printeragent.js | 20 ++- src/providers/mssql/schema.js | 22 +++- src/providers/mssql/upgrade/3_to_4.js | 31 +++++ src/providers/sqlite3/downgrade/4_to_3.js | 28 +++++ src/providers/sqlite3/queries/agentprinter.js | 15 +++ src/providers/sqlite3/schema.js | 20 ++- src/providers/sqlite3/upgrade/3_to_4.js | 30 +++++ src/store/agentprinter/_shared.js | 16 +++ src/store/agentprinter/delete.js | 35 ++++++ src/store/agentprinter/findOrCreate.js | 55 ++++++++ src/store/agentprinter/getById.js | 36 ++++++ src/store/agentprinter/list.js | 41 ++++++ src/store/agentprinter/order.js | 59 +++++++++ src/store/agentprinter/update.js | 63 ++++++++++ src/store/printeragent/_shared.js | 6 +- src/store/printeragent/create.js | 3 +- src/store/printeragent/pair.js | 4 - src/store/printeragent/update.js | 6 +- 25 files changed, 757 insertions(+), 40 deletions(-) create mode 100644 API.agentprinter.md create mode 100644 src/providers/mssql/downgrade/4_to_3.js create mode 100644 src/providers/mssql/queries/agentprinter.js create mode 100644 src/providers/mssql/upgrade/3_to_4.js create mode 100644 src/providers/sqlite3/downgrade/4_to_3.js create mode 100644 src/providers/sqlite3/queries/agentprinter.js create mode 100644 src/providers/sqlite3/upgrade/3_to_4.js create mode 100644 src/store/agentprinter/_shared.js create mode 100644 src/store/agentprinter/delete.js create mode 100644 src/store/agentprinter/findOrCreate.js create mode 100644 src/store/agentprinter/getById.js create mode 100644 src/store/agentprinter/list.js create mode 100644 src/store/agentprinter/order.js create mode 100644 src/store/agentprinter/update.js diff --git a/API.agentprinter.md b/API.agentprinter.md new file mode 100644 index 0000000..dea9f8b --- /dev/null +++ b/API.agentprinter.md @@ -0,0 +1,119 @@ +# agentprinter + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `agentprinter.delete` + +*Source: `src/store/agentprinter/delete.js`* + +Deletes an agent printer by id for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The agent printer id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number}>` + +## `agentprinter.findOrCreate` + +*Source: `src/store/agentprinter/findOrCreate.js`* + +Finds or creates an agentprinter by (printeragentId, type, model, serial) combo. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `printeragentId` | `number` | The parent printer agent id. | +| `input` | `object` | | +| `input.type` | `string` | Printer type ("zpl", "esc/pos", etc.). | +| `[input.model]` | `string|null` | Discovered model name. | +| `[input.serial]` | `string|null` | Discovered serial number. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>` + +## `agentprinter.getById` + +*Source: `src/store/agentprinter/getById.js`* + +Retrieves an agent printer by id, scoped to the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The agent printer id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>` + +## `agentprinter.list` + +*Source: `src/store/agentprinter/list.js`* + +Lists agent printers of the user's selected company, optionally filtered by printer agent. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[printeragentId]` | `number` | Optional printer agent id filter. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` + +## `agentprinter.order` + +*Source: `src/store/agentprinter/order.js`* + +Reorders agent printers for a given printer agent within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | | +| `input.printeragentId` | `number` | The printer agent id. | +| `input.agentprinterIds` | `number[]` | Complete ordered list of agent printer IDs. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{agentprinterIds: number[]}>` + +## `agentprinter.update` + +*Source: `src/store/agentprinter/update.js`* + +Updates enabled and/or sort of an agent printer for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | | +| `input.id` | `number` | Agent printer id. | +| `[input.enabled]` | `boolean` | Whether the printer is enabled. | +| `[input.sort]` | `number` | Display order integer. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>` diff --git a/API.md b/API.md index 97feef5..56b66f6 100644 --- a/API.md +++ b/API.md @@ -344,6 +344,13 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `list(googleAuthId: number, tx?: object): Promise>` +- [agentprinter](API.agentprinter.md) + - `delete(googleAuthId: number, id: number, tx?: object): Promise<{id: number}>` + - `findOrCreate(printeragentId: number, input: {type: string, model?: string|null, serial?: string|null}, tx?: object): Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>` + - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>` + - `list(googleAuthId: number, printeragentId?: number, tx?: object): Promise>` + - `order(googleAuthId: number, input: {printeragentId: number, agentprinterIds: number[]}, tx?: object): Promise<{agentprinterIds: number[]}>` + - `update(googleAuthId: number, input: {id: number, enabled?: boolean, sort?: number}, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>` - [articlehistory](API.articlehistory.md) - `record(): void` - [articlelist](API.articlelist.md) diff --git a/scripts/test-printeragent.js b/scripts/test-printeragent.js index 61686a5..3bae563 100644 --- a/scripts/test-printeragent.js +++ b/scripts/test-printeragent.js @@ -34,42 +34,78 @@ try { // pair const paired = await store.printeragent.pair(a.id, { token: 'test-token-123', - clientserial: 'SN12345', - clientmodel: 'Epson TM-T88VI', agentversion: '1.0.0', }); assert.equal(paired.token, 'test-token-123'); - assert.equal(paired.clientserial, 'SN12345'); - assert.equal(paired.clientmodel, 'Epson TM-T88VI'); assert.equal(paired.agentversion, '1.0.0'); assert.equal(paired.pairingpin, null); + assert.equal(paired.enabled, true); assert.ok(paired.pairedAt); // findByToken const foundByToken = await store.printeragent.findByToken('test-token-123'); assert.equal(foundByToken.id, a.id); - assert.equal(foundByToken.clientmodel, 'Epson TM-T88VI'); + assert.equal(foundByToken.enabled, true); assert.equal(await store.printeragent.findByToken('invalid-token'), null); - // updateState - const withState = await store.printeragent.updateState(a.id, { online: true, paper: 'ok' }); - assert.equal(withState.clientstate, JSON.stringify({ online: true, paper: 'ok' })); - // touchLastseen await store.printeragent.touchLastseen(a.id, '1.0.1'); const afterTouch = await store.printeragent.findByToken('test-token-123'); assert.equal(afterTouch.agentversion, '1.0.1'); - // update name and sort - const b = await store.printeragent.update(googleAuthId, { id: a.id, name: 'Kitchen', sort: 1 }); + // update name, sort, and enabled + const b = await store.printeragent.update(googleAuthId, { id: a.id, name: 'Kitchen', sort: 1, enabled: false }); assert.equal(b.name, 'Kitchen'); assert.equal(b.sort, 1); + assert.equal(b.enabled, false); // list const list = await store.printeragent.list(googleAuthId); assert.equal(list.length, 1); assert.equal(list[0].name, 'Kitchen'); assert.equal(list[0].sort, 1); + assert.equal(list[0].enabled, false); + + // test agentprinter findOrCreate + const ap1 = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' }); + assert.equal(ap1.created, true); + assert.equal(ap1.agentprinter.type, 'zpl'); + assert.equal(ap1.agentprinter.model, 'ZD410-200dpi'); + assert.equal(ap1.agentprinter.serial, '50J203002218'); + assert.equal(ap1.agentprinter.enabled, true); + + // second call with same combo returns existing + const ap1Again = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' }); + assert.equal(ap1Again.created, false); + assert.equal(ap1Again.agentprinter.id, ap1.agentprinter.id); + + // create second printer + const ap2 = await store.agentprinter.findOrCreate(a.id, { type: 'esc/pos', model: 'TM-T88VI', serial: 'SN999' }); + assert.equal(ap2.created, true); + + // list agentprinters + const apList = await store.agentprinter.list(googleAuthId, a.id); + assert.equal(apList.length, 2); + + // getById + const apGet = await store.agentprinter.getById(googleAuthId, ap1.agentprinter.id); + assert.equal(apGet.serial, '50J203002218'); + + // update agentprinter + const apUpdated = await store.agentprinter.update(googleAuthId, { id: ap1.agentprinter.id, enabled: false, sort: 10 }); + assert.equal(apUpdated.enabled, false); + assert.equal(apUpdated.sort, 10); + + // order agentprinters + const ordered = await store.agentprinter.order(googleAuthId, { + printeragentId: a.id, + agentprinterIds: [ap2.agentprinter.id, ap1.agentprinter.id], + }); + assert.deepEqual(ordered.agentprinterIds, [ap2.agentprinter.id, ap1.agentprinter.id]); + + // delete agentprinter + await store.agentprinter.delete(googleAuthId, ap2.agentprinter.id); + assert.equal((await store.agentprinter.list(googleAuthId, a.id)).length, 1); // delete await store.printeragent.delete(googleAuthId, a.id); diff --git a/src/index.js b/src/index.js index 9144f57..af56fc8 100644 --- a/src/index.js +++ b/src/index.js @@ -209,6 +209,12 @@ import printeragentFindByToken from './store/printeragent/findByToken.js'; import printeragentPair from './store/printeragent/pair.js'; import printeragentUpdateState from './store/printeragent/updateState.js'; import printeragentTouchLastseen from './store/printeragent/touchLastseen.js'; +import agentprinterList from './store/agentprinter/list.js'; +import agentprinterGetById from './store/agentprinter/getById.js'; +import agentprinterFindOrCreate from './store/agentprinter/findOrCreate.js'; +import agentprinterUpdate from './store/agentprinter/update.js'; +import agentprinterOrder from './store/agentprinter/order.js'; +import agentprinterDelete from './store/agentprinter/delete.js'; import modelconversationCreate from './store/modelconversation/create.js'; import modelconversationGet from './store/modelconversation/get.js'; import modelconversationList from './store/modelconversation/list.js'; @@ -658,6 +664,15 @@ export default class LibDashStore { touchLastseen: printeragentTouchLastseen(provider), }; + this.agentprinter = { + list: agentprinterList(provider), + getById: agentprinterGetById(provider), + findOrCreate: agentprinterFindOrCreate(provider), + update: agentprinterUpdate(provider), + order: agentprinterOrder(provider), + delete: agentprinterDelete(provider), + }; + this.modelconversation = { create: modelconversationCreate(provider), get: modelconversationGet(provider), diff --git a/src/providers/mssql/downgrade/4_to_3.js b/src/providers/mssql/downgrade/4_to_3.js new file mode 100644 index 0000000..85ae882 --- /dev/null +++ b/src/providers/mssql/downgrade/4_to_3.js @@ -0,0 +1,38 @@ +/** + * Schema 4 → 3 (MSSQL): + * - Drops agentprinter table + * - Removes enabled from printeragent + * - Restores clientserial, clientmodel, clientstate in printeragent + * @param {{ query: (sql: string, params?: object) => Promise, assertVersion: (v: number) => Promise, setVersion: (v: number) => Promise }} ctx + */ +export default async function downgrade({ query, assertVersion, setVersion }) { + await assertVersion(4); + + await query(`DROP TABLE IF EXISTS agentprinter;`); + + await query(` + IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL + BEGIN + IF COL_LENGTH('printeragent', 'enabled') IS NOT NULL + BEGIN + DECLARE @defName NVARCHAR(255); + SELECT @defName = dc.name + FROM sys.default_constraints dc + JOIN sys.columns c ON c.default_object_id = dc.object_id + WHERE dc.parent_object_id = OBJECT_ID(N'dbo.printeragent') AND c.name = 'enabled'; + IF @defName IS NOT NULL + EXEC('ALTER TABLE printeragent DROP CONSTRAINT ' + @defName); + ALTER TABLE printeragent DROP COLUMN enabled; + END + IF COL_LENGTH('printeragent', 'clientserial') IS NULL + ALTER TABLE printeragent ADD clientserial NVARCHAR(255) NULL; + IF COL_LENGTH('printeragent', 'clientmodel') IS NULL + ALTER TABLE printeragent ADD clientmodel NVARCHAR(255) NULL; + IF COL_LENGTH('printeragent', 'clientstate') IS NULL + ALTER TABLE printeragent ADD clientstate NVARCHAR(255) NULL; + END + `); + + await setVersion(3); +} + diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index df0e2f5..8095100 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -28,6 +28,7 @@ export const TABLES = [ 'posterminalobituary', 'posterminalcategory', 'posterminal', + 'agentprinter', 'printeragent', 'printer', 'numbers', diff --git a/src/providers/mssql/queries/agentprinter.js b/src/providers/mssql/queries/agentprinter.js new file mode 100644 index 0000000..706d2ec --- /dev/null +++ b/src/providers/mssql/queries/agentprinter.js @@ -0,0 +1,69 @@ +export const AGENTPRINTER_COLUMNS = ` + ap.id, + ap.printeragent_id AS printeragentId, + ap.type, + ap.model, + ap.serial, + ap.enabled, + ap.sort +`; + +export const LIST_BY_COMPANY = ` +SELECT +${AGENTPRINTER_COLUMNS} +FROM agentprinter ap +JOIN printeragent pa ON pa.id = ap.printeragent_id +WHERE pa.company_id = @companyId +ORDER BY ap.sort, ap.id +`; + +export const LIST_BY_PRINTERAGENT = ` +SELECT +${AGENTPRINTER_COLUMNS} +FROM agentprinter ap +JOIN printeragent pa ON pa.id = ap.printeragent_id +WHERE ap.printeragent_id = @printeragentId AND pa.company_id = @companyId +ORDER BY ap.sort, ap.id +`; + +export const GET_BY_ID = ` +SELECT +${AGENTPRINTER_COLUMNS} +FROM agentprinter ap +JOIN printeragent pa ON pa.id = ap.printeragent_id +WHERE ap.id = @id AND pa.company_id = @companyId +`; + +export const FIND_BY_COMBO = ` +SELECT +${AGENTPRINTER_COLUMNS} +FROM agentprinter ap +WHERE ap.printeragent_id = @printeragentId + AND ap.type = @type + AND (ap.serial = @serial OR (ap.serial IS NULL AND @serial IS NULL)) + AND (ap.model = @model OR (ap.model IS NULL AND @model IS NULL)) +`; + +export const CREATE = ` +INSERT INTO agentprinter (printeragent_id, type, model, serial, enabled, sort) +OUTPUT + INSERTED.id, + INSERTED.printeragent_id AS printeragentId, + INSERTED.type, + INSERTED.model, + INSERTED.serial, + INSERTED.enabled, + INSERTED.sort +VALUES (@printeragentId, @type, @model, @serial, @enabled, @sort) +`; + +export const UPDATE = ` +UPDATE agentprinter +SET enabled = @enabled, sort = @sort +WHERE id = @id AND printeragent_id IN (SELECT id FROM printeragent WHERE company_id = @companyId) +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM agentprinter +WHERE id = @id AND printeragent_id IN (SELECT id FROM printeragent WHERE company_id = @companyId) +`; diff --git a/src/providers/mssql/queries/printeragent.js b/src/providers/mssql/queries/printeragent.js index 4f26d52..f311b7e 100644 --- a/src/providers/mssql/queries/printeragent.js +++ b/src/providers/mssql/queries/printeragent.js @@ -5,13 +5,11 @@ export const PRINTERAGENT_COLUMNS = ` token, pairingpin, paired_at AS pairedAt, - clientserial, - clientmodel, - clientstate, agentversion, lastseen_at AS lastseenAt, updated_at AS updatedAt, - sort + sort, + enabled `; export const LIST_BY_COMPANY = ` @@ -55,8 +53,6 @@ UPDATE printeragent SET token = @token, pairingpin = NULL, paired_at = @pairedAt, - clientserial = @clientserial, - clientmodel = @clientmodel, agentversion = @agentversion, lastseen_at = SYSUTCDATETIME(), updated_at = SYSUTCDATETIME() @@ -85,7 +81,7 @@ WHERE id = @id `; export const CREATE = ` -INSERT INTO printeragent (company_id, name, pairingpin, sort) +INSERT INTO printeragent (company_id, name, pairingpin, sort, enabled) OUTPUT INSERTED.id, INSERTED.company_id AS companyId, @@ -93,19 +89,17 @@ OUTPUT INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, - INSERTED.clientserial, - INSERTED.clientmodel, - INSERTED.clientstate, INSERTED.agentversion, INSERTED.lastseen_at AS lastseenAt, INSERTED.updated_at AS updatedAt, - INSERTED.sort -VALUES (@companyId, @name, @pairingpin, @sort) + INSERTED.sort, + INSERTED.enabled +VALUES (@companyId, @name, @pairingpin, @sort, @enabled) `; export const UPDATE = ` UPDATE printeragent -SET name = @name, sort = @sort, updated_at = SYSUTCDATETIME() +SET name = @name, sort = @sort, enabled = @enabled, updated_at = SYSUTCDATETIME() WHERE id = @id AND company_id = @companyId `; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index c21bff2..e97b740 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -1021,13 +1021,11 @@ BEGIN token NVARCHAR(255) NULL, pairingpin NVARCHAR(50) NULL, paired_at DATETIME2 NULL, - clientserial NVARCHAR(255) NULL, - clientmodel NVARCHAR(255) NULL, - clientstate NVARCHAR(255) NULL, agentversion NVARCHAR(255) NULL, lastseen_at DATETIME2 NULL, updated_at DATETIME2 NOT NULL CONSTRAINT DF_printeragent_updated_at DEFAULT SYSUTCDATETIME(), sort INT NOT NULL CONSTRAINT DF_printeragent_sort DEFAULT 0, + enabled BIT NOT NULL CONSTRAINT DF_printeragent_enabled DEFAULT 1, CONSTRAINT FK_printeragent_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE ); CREATE UNIQUE INDEX UQ_printeragent_token ON printeragent(token) WHERE token IS NOT NULL; @@ -1035,6 +1033,23 @@ BEGIN END `; +export const INIT_AGENTPRINTER = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'agentprinter') +BEGIN + CREATE TABLE agentprinter ( + id INT IDENTITY(1,1) PRIMARY KEY, + printeragent_id INT NOT NULL, + type NVARCHAR(50) NOT NULL, + model NVARCHAR(255) NULL, + serial NVARCHAR(255) NULL, + enabled BIT NOT NULL CONSTRAINT DF_agentprinter_enabled DEFAULT 1, + sort INT NOT NULL CONSTRAINT DF_agentprinter_sort DEFAULT 0, + CONSTRAINT FK_agentprinter_printeragent FOREIGN KEY (printeragent_id) REFERENCES printeragent(id) ON DELETE CASCADE + ); + CREATE INDEX IX_agentprinter_printeragent_id ON agentprinter(printeragent_id); +END +`; + export const INIT_LABELTEMPLATE = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'labeltemplate') BEGIN @@ -1691,6 +1706,7 @@ export const SCHEMA_SCRIPTS = [ INIT_POSTERMINALSYNCQUEUE, INIT_PRINTER, INIT_PRINTERAGENT, + INIT_AGENTPRINTER, INIT_LABELTEMPLATE, INIT_FILE, INIT_MANIFESTATTACHMENT, diff --git a/src/providers/mssql/upgrade/3_to_4.js b/src/providers/mssql/upgrade/3_to_4.js new file mode 100644 index 0000000..ab97f18 --- /dev/null +++ b/src/providers/mssql/upgrade/3_to_4.js @@ -0,0 +1,31 @@ +import { INIT_AGENTPRINTER } from '../schema.js'; + +/** + * Schema 3 → 4 (MSSQL): + * - Drops clientstate, clientmodel, clientserial from printeragent + * - Adds enabled to printeragent + * - Creates agentprinter table + * @param {{ query: (sql: string, params?: object) => Promise, assertVersion: (v: number) => Promise, setVersion: (v: number) => Promise }} ctx + */ +export default async function upgrade({ query, assertVersion, setVersion }) { + await assertVersion(3); + + await query(` + IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL + BEGIN + IF COL_LENGTH('printeragent', 'clientstate') IS NOT NULL + ALTER TABLE printeragent DROP COLUMN clientstate; + IF COL_LENGTH('printeragent', 'clientmodel') IS NOT NULL + ALTER TABLE printeragent DROP COLUMN clientmodel; + IF COL_LENGTH('printeragent', 'clientserial') IS NOT NULL + ALTER TABLE printeragent DROP COLUMN clientserial; + IF COL_LENGTH('printeragent', 'enabled') IS NULL + ALTER TABLE printeragent ADD enabled BIT NOT NULL CONSTRAINT DF_printeragent_enabled DEFAULT 1; + END + `); + + await query(INIT_AGENTPRINTER); + + await setVersion(4); +} + diff --git a/src/providers/sqlite3/downgrade/4_to_3.js b/src/providers/sqlite3/downgrade/4_to_3.js new file mode 100644 index 0000000..560fab1 --- /dev/null +++ b/src/providers/sqlite3/downgrade/4_to_3.js @@ -0,0 +1,28 @@ +/** + * Schema 4 → 3 (SQLite): + * - Drops agentprinter table + * - Removes enabled from printeragent + * - Restores clientserial, clientmodel, clientstate in printeragent + * @param {{ exec: (sql: string) => void, assertVersion: (v: number) => Promise, setVersion: (v: number) => Promise }} ctx + */ +export default async function downgrade({ exec, assertVersion, setVersion }) { + await assertVersion(4); + + exec(`DROP TABLE IF EXISTS agentprinter;`); + + try { + exec(`ALTER TABLE printeragent DROP COLUMN enabled;`); + } catch {} + try { + exec(`ALTER TABLE printeragent ADD COLUMN clientserial TEXT;`); + } catch {} + try { + exec(`ALTER TABLE printeragent ADD COLUMN clientmodel TEXT;`); + } catch {} + try { + exec(`ALTER TABLE printeragent ADD COLUMN clientstate TEXT;`); + } catch {} + + await setVersion(3); +} + diff --git a/src/providers/sqlite3/queries/agentprinter.js b/src/providers/sqlite3/queries/agentprinter.js new file mode 100644 index 0000000..94c7936 --- /dev/null +++ b/src/providers/sqlite3/queries/agentprinter.js @@ -0,0 +1,15 @@ +import * as source from '../../mssql/queries/agentprinter.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'agentprinter'); +export const { + AGENTPRINTER_COLUMNS, + CREATE, + DELETE_BY_COMPANY_AND_ID, + FIND_BY_COMBO, + GET_BY_ID, + LIST_BY_COMPANY, + LIST_BY_PRINTERAGENT, + UPDATE, +} = queries; + diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 6090557..b4ba58f 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -700,13 +700,11 @@ CREATE TABLE IF NOT EXISTS printeragent ( token TEXT, pairingpin TEXT, paired_at TEXT, - clientserial TEXT, - clientmodel TEXT, - clientstate TEXT, agentversion TEXT, lastseen_at TEXT, updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), sort INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS UQ_printeragent_token @@ -715,6 +713,21 @@ CREATE UNIQUE INDEX IF NOT EXISTS UQ_printeragent_pairingpin ON printeragent(pairingpin) WHERE pairingpin IS NOT NULL; `; +export const INIT_AGENTPRINTER = ` +CREATE TABLE IF NOT EXISTS agentprinter ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + printeragent_id INTEGER NOT NULL, + type TEXT NOT NULL, + model TEXT, + serial TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (printeragent_id) REFERENCES printeragent(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_agentprinter_printeragent_id + ON agentprinter(printeragent_id); +`; + export const INIT_LABELTEMPLATE = ` CREATE TABLE IF NOT EXISTS labeltemplate ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1249,6 +1262,7 @@ export const SCHEMA_SCRIPTS = [ INIT_POSTERMINALSYNCQUEUE, INIT_PRINTER, INIT_PRINTERAGENT, + INIT_AGENTPRINTER, INIT_LABELTEMPLATE, INIT_FILE, INIT_MANIFESTATTACHMENT, diff --git a/src/providers/sqlite3/upgrade/3_to_4.js b/src/providers/sqlite3/upgrade/3_to_4.js new file mode 100644 index 0000000..a7ffd55 --- /dev/null +++ b/src/providers/sqlite3/upgrade/3_to_4.js @@ -0,0 +1,30 @@ +import { INIT_AGENTPRINTER } from '../schema.js'; + +/** + * Schema 3 → 4 (SQLite): + * - Drops clientstate, clientmodel, clientserial from printeragent + * - Adds enabled to printeragent + * - Creates agentprinter table + * @param {{ exec: (sql: string) => void, assertVersion: (v: number) => Promise, setVersion: (v: number) => Promise }} ctx + */ +export default async function upgrade({ exec, assertVersion, setVersion }) { + await assertVersion(3); + + try { + exec(`ALTER TABLE printeragent DROP COLUMN clientstate;`); + } catch {} + try { + exec(`ALTER TABLE printeragent DROP COLUMN clientmodel;`); + } catch {} + try { + exec(`ALTER TABLE printeragent DROP COLUMN clientserial;`); + } catch {} + try { + exec(`ALTER TABLE printeragent ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;`); + } catch {} + + exec(INIT_AGENTPRINTER); + + await setVersion(4); +} + diff --git a/src/store/agentprinter/_shared.js b/src/store/agentprinter/_shared.js new file mode 100644 index 0000000..f42938a --- /dev/null +++ b/src/store/agentprinter/_shared.js @@ -0,0 +1,16 @@ +export function shape(row) { + if (!row) return null; + return { + id: row.id, + printeragentId: row.printeragentId, + type: row.type, + model: row.model ?? null, + serial: row.serial ?? null, + enabled: Boolean(row.enabled), + sort: row.sort, + }; +} + +export const AGENTPRINTER_TYPEDEF = + '{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}'; + diff --git a/src/store/agentprinter/delete.js b/src/store/agentprinter/delete.js new file mode 100644 index 0000000..a83a54f --- /dev/null +++ b/src/store/agentprinter/delete.js @@ -0,0 +1,35 @@ +import { + DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE, +} from '../../providers/sqlite3/queries/agentprinter.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 an agent printer by id for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The agent printer id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number}>} + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Agentprinter not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + await runner.query(sql, { id: numericId, companyId }); + return { id: numericId }; + }; +} + diff --git a/src/store/agentprinter/findOrCreate.js b/src/store/agentprinter/findOrCreate.js new file mode 100644 index 0000000..33957f4 --- /dev/null +++ b/src/store/agentprinter/findOrCreate.js @@ -0,0 +1,55 @@ +import { + FIND_BY_COMBO as MSSQL_FIND, + CREATE as MSSQL_CREATE, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + FIND_BY_COMBO as SQLITE_FIND, + CREATE as SQLITE_CREATE, +} from '../../providers/sqlite3/queries/agentprinter.js'; +import makeRunner from '../runner.js'; +import { shape } from './_shared.js'; + +export default function findOrCreate(provider) { + const isSqlite = provider?.dialect === 'sqlite3'; + const findSql = isSqlite ? SQLITE_FIND : MSSQL_FIND; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Finds or creates an agentprinter by (printeragentId, type, model, serial) combo. + * + * @param {number} printeragentId - The parent printer agent id. + * @param {object} input + * @param {string} input.type - Printer type ("zpl", "esc/pos", etc.). + * @param {string|null} [input.model] - Discovered model name. + * @param {string|null} [input.serial] - Discovered serial number. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>} + */ + return async (printeragentId, input = {}, tx) => { + const pId = Number(printeragentId); + if (!Number.isInteger(pId) || pId <= 0) { + throw new Error('Printeragent not found'); + } + + const type = typeof input.type === 'string' && input.type.trim() ? input.type.trim().toLowerCase() : 'zpl'; + const model = input.model != null && String(input.model).trim() ? String(input.model).trim() : null; + const serial = input.serial != null && String(input.serial).trim() ? String(input.serial).trim() : null; + + const runner = makeRunner(provider, tx); + const existing = await runner.query(findSql, { printeragentId: pId, type, model, serial }); + if (existing.rows.length > 0) { + return { created: false, agentprinter: shape(existing.rows[0]) }; + } + + const created = await runner.query(createSql, { + printeragentId: pId, + type, + model, + serial, + enabled: 1, + sort: 0, + }); + return { created: true, agentprinter: shape(created.rows[0]) }; + }; +} + diff --git a/src/store/agentprinter/getById.js b/src/store/agentprinter/getById.js new file mode 100644 index 0000000..aa63643 --- /dev/null +++ b/src/store/agentprinter/getById.js @@ -0,0 +1,36 @@ +import { + GET_BY_ID as MSSQL_GET, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + GET_BY_ID as SQLITE_GET, +} from '../../providers/sqlite3/queries/agentprinter.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { shape } from './_shared.js'; + +export default function getById(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + + /** + * Retrieves an agent printer by id, scoped to the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The agent printer id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>} + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + return null; + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { id: numericId, companyId }); + return result.rows[0] ? shape(result.rows[0]) : null; + }; +} + diff --git a/src/store/agentprinter/list.js b/src/store/agentprinter/list.js new file mode 100644 index 0000000..cb50273 --- /dev/null +++ b/src/store/agentprinter/list.js @@ -0,0 +1,41 @@ +import { + LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY, + LIST_BY_PRINTERAGENT as MSSQL_LIST_BY_PRINTERAGENT, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY, + LIST_BY_PRINTERAGENT as SQLITE_LIST_BY_PRINTERAGENT, +} from '../../providers/sqlite3/queries/agentprinter.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { shape } from './_shared.js'; + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listCompanySql = isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY; + const listAgentSql = isSqlite ? SQLITE_LIST_BY_PRINTERAGENT : MSSQL_LIST_BY_PRINTERAGENT; + + /** + * Lists agent printers of the user's selected company, optionally filtered by printer agent. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} [printeragentId] - Optional printer agent id filter. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} + */ + return async (googleAuthId, printeragentId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const aId = printeragentId != null ? Number(printeragentId) : null; + let result; + if (aId && Number.isInteger(aId) && aId > 0) { + result = await runner.query(listAgentSql, { companyId, printeragentId: aId }); + } else { + result = await runner.query(listCompanySql, { companyId }); + } + return result.rows.map(shape); + }; +} + diff --git a/src/store/agentprinter/order.js b/src/store/agentprinter/order.js new file mode 100644 index 0000000..f3812a2 --- /dev/null +++ b/src/store/agentprinter/order.js @@ -0,0 +1,59 @@ +import { + LIST_BY_PRINTERAGENT as MSSQL_LIST, + UPDATE as MSSQL_UPDATE, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + LIST_BY_PRINTERAGENT as SQLITE_LIST, + UPDATE as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/agentprinter.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function order(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listSql = isSqlite ? SQLITE_LIST : MSSQL_LIST; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Reorders agent printers for a given printer agent within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input + * @param {number} input.printeragentId - The printer agent id. + * @param {number[]} input.agentprinterIds - Complete ordered list of agent printer IDs. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{agentprinterIds: number[]}>} + */ + return async (googleAuthId, input = {}, tx) => { + const pId = Number(input.printeragentId); + if (!Number.isInteger(pId) || pId <= 0) { + throw new Error('Printeragent not found'); + } + + const ids = input.agentprinterIds; + if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => !Number.isInteger(Number(id)) || Number(id) <= 0)) { + throw new Error('Invalid agentprinter order'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const existing = await runner.query(listSql, { companyId, printeragentId: pId }); + const existingIds = new Set(existing.rows.map((r) => r.id)); + + if (ids.length !== existingIds.size || ids.some((id) => !existingIds.has(Number(id)))) { + throw new Error('Invalid agentprinter order'); + } + + for (let i = 0; i < ids.length; i++) { + const id = Number(ids[i]); + const current = existing.rows.find((r) => r.id === id); + const enabled = current ? (current.enabled ? 1 : 0) : 1; + await runner.query(updateSql, { id, companyId, sort: i + 1, enabled }); + } + + return { agentprinterIds: ids.map(Number) }; + }; +} + diff --git a/src/store/agentprinter/update.js b/src/store/agentprinter/update.js new file mode 100644 index 0000000..d916f9b --- /dev/null +++ b/src/store/agentprinter/update.js @@ -0,0 +1,63 @@ +import { + GET_BY_ID as MSSQL_GET, + UPDATE as MSSQL_UPDATE, +} from '../../providers/mssql/queries/agentprinter.js'; +import { + GET_BY_ID as SQLITE_GET, + UPDATE as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/agentprinter.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { shape } from './_shared.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; + + /** + * Updates enabled and/or sort of an agent printer for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input + * @param {number} input.id - Agent printer id. + * @param {boolean} [input.enabled] - Whether the printer is enabled. + * @param {number} [input.sort] - Display order integer. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>} + */ + return async (googleAuthId, input = {}, tx) => { + const id = Number(input.id); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Agentprinter not found'); + } + + if (input.enabled === undefined && input.sort === undefined) { + throw new Error('Invalid agentprinter'); + } + if (input.enabled !== undefined && typeof input.enabled !== 'boolean') { + throw new Error('Invalid agentprinter'); + } + if (input.sort !== undefined && !Number.isInteger(input.sort)) { + throw new Error('Invalid agentprinter'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const existing = await runner.query(getSql, { id, companyId }); + if (!existing.rows[0]) { + throw new Error('Agentprinter not found'); + } + + const sort = input.sort !== undefined ? input.sort : existing.rows[0].sort; + const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : (existing.rows[0].enabled ? 1 : 0); + + await runner.query(updateSql, { id, companyId, sort, enabled }); + + const updated = await runner.query(getSql, { id, companyId }); + return shape(updated.rows[0]); + }; +} + diff --git a/src/store/printeragent/_shared.js b/src/store/printeragent/_shared.js index b0f2443..51bdc9a 100644 --- a/src/store/printeragent/_shared.js +++ b/src/store/printeragent/_shared.js @@ -6,15 +6,13 @@ export function shape(row) { token: row.token ?? null, pairingpin: row.pairingpin ?? null, pairedAt: row.pairedAt ?? null, - clientserial: row.clientserial ?? null, - clientmodel: row.clientmodel ?? null, - clientstate: row.clientstate ?? null, agentversion: row.agentversion ?? null, lastseenAt: row.lastseenAt ?? null, updatedAt: row.updatedAt, sort: row.sort, + enabled: Boolean(row.enabled), }; } export const PRINTERAGENT_TYPEDEF = - '{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}'; + '{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean}'; diff --git a/src/store/printeragent/create.js b/src/store/printeragent/create.js index 6194124..000b3d4 100644 --- a/src/store/printeragent/create.js +++ b/src/store/printeragent/create.js @@ -39,11 +39,12 @@ export default function create(provider) { throw new Error('Invalid printeragent'); } const sort = input.sort ?? 0; + const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : 1; const runner = makeRunner(provider, tx); const { companyId } = await access(googleAuthId, runner); - const result = await runner.query(sql, { companyId, name, pairingpin, sort }); + const result = await runner.query(sql, { companyId, name, pairingpin, sort, enabled }); return shape(result.rows[0]); }; } diff --git a/src/store/printeragent/pair.js b/src/store/printeragent/pair.js index a283685..59483bf 100644 --- a/src/store/printeragent/pair.js +++ b/src/store/printeragent/pair.js @@ -37,8 +37,6 @@ export default function pair(provider) { throw new Error('Invalid printeragent'); } - const clientserial = input.clientserial ? String(input.clientserial).trim() : null; - const clientmodel = input.clientmodel ? String(input.clientmodel).trim() : null; const agentversion = input.agentversion ? String(input.agentversion).trim() : null; const pairedAt = input.pairedAt ?? new Date().toISOString(); @@ -47,8 +45,6 @@ export default function pair(provider) { id: numericId, token, pairedAt, - clientserial, - clientmodel, agentversion, }); diff --git a/src/store/printeragent/update.js b/src/store/printeragent/update.js index 344f79e..ba606e2 100644 --- a/src/store/printeragent/update.js +++ b/src/store/printeragent/update.js @@ -36,7 +36,7 @@ export default function update(provider) { if (!Number.isInteger(id) || id <= 0) { throw new Error('Printeragent not found'); } - if (input.name === undefined && input.sort === undefined) { + if (input.name === undefined && input.sort === undefined && input.enabled === undefined) { throw new Error('Invalid printeragent'); } if (input.name !== undefined && (typeof input.name !== 'string' || !input.name.trim())) { @@ -45,6 +45,9 @@ export default function update(provider) { if (input.sort !== undefined && !Number.isInteger(input.sort)) { throw new Error('Invalid printeragent'); } + if (input.enabled !== undefined && typeof input.enabled !== 'boolean') { + throw new Error('Invalid printeragent'); + } const runner = makeRunner(provider, tx); const { companyId } = await access(googleAuthId, runner); @@ -59,6 +62,7 @@ export default function update(provider) { companyId, name: input.name !== undefined ? input.name.trim() : existing.rows[0].name, sort: input.sort !== undefined ? input.sort : existing.rows[0].sort, + enabled: input.enabled !== undefined ? (input.enabled ? 1 : 0) : existing.rows[0].enabled, }); const row = await runner.query(getSql, { id, companyId });