feat(numbers): add number sequences store; restrict companysetting keys
- Add `numbers` store module with `list` and `update` methods backed by the `numbers` table (prefix, padding, next) per company. - Add sql queries LIST_BY_COMPANY, GET_BY_COMPANY_AND_ID, UPDATE_... - Regenerate and wire API docs for `numbers`. companysetting: - Remove the `delete` endpoint (settings are now upsert/read only). - Restrict `set` to a fixed allow-list of keys: ui.theme, keys.tavily, keys.openrouter, labelprinter.addr, labelprinter.type. - Drop DELETE_BY_COMPANY_AND_KEY sql and the delete store file. Docs: update README, API.md, API.companysetting.md, API.numbers.md.
This commit is contained in:
@@ -2,33 +2,6 @@
|
|||||||
|
|
||||||
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
## `companysetting.delete`
|
|
||||||
|
|
||||||
*Source: `src/store/companysetting/delete.js`*
|
|
||||||
|
|
||||||
Deletes a company setting by key for the user's selected company.
|
|
||||||
|
|
||||||
**Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
|
||||||
| `key` | `string` | The setting key. |
|
|
||||||
|
|
||||||
**Returns**
|
|
||||||
|
|
||||||
`Promise<void>`
|
|
||||||
|
|
||||||
**Throws**
|
|
||||||
|
|
||||||
`Error` — If the setting is not found.
|
|
||||||
|
|
||||||
**Example**
|
|
||||||
|
|
||||||
```js
|
|
||||||
await store.companysetting.delete(42, 'theme');
|
|
||||||
```
|
|
||||||
|
|
||||||
## `companysetting.get`
|
## `companysetting.get`
|
||||||
|
|
||||||
*Source: `src/store/companysetting/get.js`*
|
*Source: `src/store/companysetting/get.js`*
|
||||||
@@ -84,7 +57,7 @@ const settings = await store.companysetting.list(42, { frontendOnly: true });
|
|||||||
|
|
||||||
*Source: `src/store/companysetting/set.js`*
|
*Source: `src/store/companysetting/set.js`*
|
||||||
|
|
||||||
Sets (upserts) a company setting by key for the user's selected company.
|
Sets (upserts) a company setting by key for the user's selected company. Only a fixed allow-list of keys can be set: `ui.theme`, `keys.tavily`, `keys.openrouter`, `labelprinter.addr`, `labelprinter.type`.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -92,7 +65,7 @@ Sets (upserts) a company setting by key for the user's selected company.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `input` | `object` | The setting input. |
|
| `input` | `object` | The setting input. |
|
||||||
| `input.key` | `string` | The setting key. |
|
| `input.key` | `string` | The setting key (must be in the allow-list). |
|
||||||
| `input.value` | `string` | The setting value. |
|
| `input.value` | `string` | The setting value. |
|
||||||
| `[input.frontend]` | `boolean` | Whether the setting is frontend-visible. |
|
| `[input.frontend]` | `boolean` | Whether the setting is frontend-visible. |
|
||||||
|
|
||||||
@@ -107,6 +80,6 @@ Sets (upserts) a company setting by key for the user's selected company.
|
|||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const setting = await store.companysetting.set(42, { key: 'theme', value: 'dark', frontend: true });
|
const setting = await store.companysetting.set(42, { key: 'ui.theme', value: 'dark' });
|
||||||
// => { key, value, frontend }
|
// => { key, value, frontend }
|
||||||
```
|
```
|
||||||
|
|||||||
1
API.md
1
API.md
@@ -26,3 +26,4 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- [posterminal](API.posterminal.md)
|
- [posterminal](API.posterminal.md)
|
||||||
- [google_auth](API.google_auth.md)
|
- [google_auth](API.google_auth.md)
|
||||||
- [pic](API.pic.md)
|
- [pic](API.pic.md)
|
||||||
|
- [numbers](API.numbers.md)
|
||||||
|
|||||||
58
API.numbers.md
Normal file
58
API.numbers.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# numbers
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `numbers.list`
|
||||||
|
|
||||||
|
*Source: `src/store/numbers/list.js`*
|
||||||
|
|
||||||
|
Lists the number sequences of the user's selected company.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>` — The number sequences.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const numbers = await store.numbers.list(42);
|
||||||
|
// => [{ id, type, prefix, padding, next }]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `numbers.update`
|
||||||
|
|
||||||
|
*Source: `src/store/numbers/update.js`*
|
||||||
|
|
||||||
|
Updates the sequence settings of a number for the user's selected company. Only the provided fields are changed (all optional). `prefix` is trimmed and must be at most 50 characters. `padding` and `next` must be finite numbers greater than or equal to 0 and 1 respectively. If no field is provided, the row is returned unchanged.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `id` | `number` | The id of the number sequence to update. |
|
||||||
|
| `input` | `object` | The update input. |
|
||||||
|
| `[input.prefix]` | `string` | The new prefix. |
|
||||||
|
| `[input.padding]` | `number` | The new padding length. |
|
||||||
|
| `[input.next]` | `number` | The new next value. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` — The updated number sequence.
|
||||||
|
|
||||||
|
**Throws**
|
||||||
|
|
||||||
|
`Error` — If the number is not found or a field is invalid.
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const number = await store.numbers.update(42, 3, { prefix: 'A-', padding: 4, next: 100 });
|
||||||
|
// => { id, type, prefix, padding, next }
|
||||||
|
```
|
||||||
19
README.md
19
README.md
@@ -163,13 +163,14 @@ Company-scoped key/value settings. Auth via selected company (`google_auth.selec
|
|||||||
| `list` | `(googleAuthId, { frontendOnly? })` | `{ key, value, frontend }[]` |
|
| `list` | `(googleAuthId, { frontendOnly? })` | `{ key, value, frontend }[]` |
|
||||||
| `get` | `(googleAuthId, key, { frontendOnly? })` | `{ key, value, frontend } \| undefined` |
|
| `get` | `(googleAuthId, key, { frontendOnly? })` | `{ key, value, frontend } \| undefined` |
|
||||||
| `set` | `(googleAuthId, { key, value, frontend? })` | `{ key, value, frontend }` |
|
| `set` | `(googleAuthId, { key, value, frontend? })` | `{ key, value, frontend }` |
|
||||||
| `delete` | `(googleAuthId, key)` | `void` |
|
|
||||||
|
|
||||||
- `value` is always a string (`NVARCHAR(MAX)`).
|
- `value` is always a string (`NVARCHAR(MAX)`).
|
||||||
- `frontend: true` → readable by frontend routes (`frontendOnly: true`). Default on `set` is `false` (backend-only).
|
- `frontend: true` → readable by frontend routes (`frontendOnly: true`). Default on `set` is `false` (backend-only).
|
||||||
- With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing).
|
- With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing).
|
||||||
|
- `set` only allows a fixed allow-list of keys: `ui.theme`, `keys.tavily`, `keys.openrouter`, `labelprinter.addr`, `labelprinter.type`.
|
||||||
|
- No delete endpoint — settings are upsert / read only.
|
||||||
|
|
||||||
Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid setting`, `Setting not found`.
|
Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid setting`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -660,6 +661,19 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `numbers`
|
||||||
|
|
||||||
|
Company-scoped number sequences (`numbers` table). Auth via selected company (`google_auth.selected_company` + membership).
|
||||||
|
|
||||||
|
| Method | Signature | Returns |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `list` | `(googleAuthId)` | `{ id, type, prefix, padding, next }[]` |
|
||||||
|
| `update` | `(googleAuthId, id, { prefix?, padding?, next? })` | `{ id, type, prefix, padding, next }` |
|
||||||
|
|
||||||
|
`list` returns every sequence for the company, ordered by `type`. `update` updates only the provided fields: `prefix` is trimmed (max 50 chars), `padding` must be an integer `>= 0`, `next` must be an integer `>= 1`. Errors: `No company selected`, `Company not found`, `Not authorized`, `Number not found`, `Invalid number`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Backend route cheatsheet
|
## Backend route cheatsheet
|
||||||
|
|
||||||
| Route | Store call |
|
| Route | Store call |
|
||||||
@@ -683,7 +697,6 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer);
|
|||||||
| `DELETE /api/unit` | `unit.delete(googleAuthId, si)` |
|
| `DELETE /api/unit` | `unit.delete(googleAuthId, si)` |
|
||||||
| `GET /api/company/:id/settings` | `companysetting.list(googleAuthId, { frontendOnly: true })` |
|
| `GET /api/company/:id/settings` | `companysetting.list(googleAuthId, { frontendOnly: true })` |
|
||||||
| `PUT /api/company/:id/settings` | `companysetting.set(googleAuthId, { key, value, frontend? })` |
|
| `PUT /api/company/:id/settings` | `companysetting.set(googleAuthId, { key, value, frontend? })` |
|
||||||
| `DELETE /api/company/:id/settings/:key` | `companysetting.delete(googleAuthId, key)` |
|
|
||||||
| `GET /api/brand` | `brand.list(googleAuthId)` |
|
| `GET /api/brand` | `brand.list(googleAuthId)` |
|
||||||
| `PUT /api/brand` | `brand.upsert(googleAuthId, { name, gsprpinfo? })` |
|
| `PUT /api/brand` | `brand.upsert(googleAuthId, { name, gsprpinfo? })` |
|
||||||
| `DELETE /api/brand/:id` | `brand.delete(googleAuthId, id)` |
|
| `DELETE /api/brand/:id` | `brand.delete(googleAuthId, id)` |
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const MODULE_ORDER = [
|
|||||||
'posterminal',
|
'posterminal',
|
||||||
'google_auth',
|
'google_auth',
|
||||||
'pic',
|
'pic',
|
||||||
|
'numbers',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Files that are internal helpers and should not appear as public API entries.
|
// Files that are internal helpers and should not appear as public API entries.
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import unitDelete from './store/unit/delete.js';
|
|||||||
import companysettingList from './store/companysetting/list.js';
|
import companysettingList from './store/companysetting/list.js';
|
||||||
import companysettingGet from './store/companysetting/get.js';
|
import companysettingGet from './store/companysetting/get.js';
|
||||||
import companysettingSet from './store/companysetting/set.js';
|
import companysettingSet from './store/companysetting/set.js';
|
||||||
import companysettingDelete from './store/companysetting/delete.js';
|
|
||||||
import brandList from './store/brand/list.js';
|
import brandList from './store/brand/list.js';
|
||||||
import brandUpsert from './store/brand/upsert.js';
|
import brandUpsert from './store/brand/upsert.js';
|
||||||
import brandDelete from './store/brand/delete.js';
|
import brandDelete from './store/brand/delete.js';
|
||||||
@@ -104,6 +103,8 @@ import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany
|
|||||||
import googleAuthSetProfile from './store/google_auth/setProfile.js';
|
import googleAuthSetProfile from './store/google_auth/setProfile.js';
|
||||||
import picGetImageByHash from './store/pic/getImageByHash.js';
|
import picGetImageByHash from './store/pic/getImageByHash.js';
|
||||||
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
import picGetThumbByHash from './store/pic/getThumbByHash.js';
|
||||||
|
import numbersList from './store/numbers/list.js';
|
||||||
|
import numbersUpdate from './store/numbers/update.js';
|
||||||
import storePurge from './store/purge.js';
|
import storePurge from './store/purge.js';
|
||||||
import storeInitialize from './store/initialize.js';
|
import storeInitialize from './store/initialize.js';
|
||||||
|
|
||||||
@@ -179,7 +180,6 @@ export default class LibDashStore {
|
|||||||
list: companysettingList(pool),
|
list: companysettingList(pool),
|
||||||
get: companysettingGet(pool),
|
get: companysettingGet(pool),
|
||||||
set: companysettingSet(pool),
|
set: companysettingSet(pool),
|
||||||
delete: companysettingDelete(pool),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.brand = {
|
this.brand = {
|
||||||
@@ -277,6 +277,11 @@ export default class LibDashStore {
|
|||||||
getThumbByHash: picGetThumbByHash(pool),
|
getThumbByHash: picGetThumbByHash(pool),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.numbers = {
|
||||||
|
list: numbersList(pool),
|
||||||
|
update: numbersUpdate(pool),
|
||||||
|
};
|
||||||
|
|
||||||
this.purge = storePurge(pool);
|
this.purge = storePurge(pool);
|
||||||
this.initialize = storeInitialize(pool);
|
this.initialize = storeInitialize(pool);
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,3 @@ WHEN NOT MATCHED THEN
|
|||||||
INSERT (company_id, [key], value, frontend)
|
INSERT (company_id, [key], value, frontend)
|
||||||
VALUES (@companyId, @key, @value, @frontend);
|
VALUES (@companyId, @key, @value, @frontend);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DELETE_BY_COMPANY_AND_KEY = `
|
|
||||||
DELETE FROM companysetting
|
|
||||||
WHERE company_id = @companyId
|
|
||||||
AND [key] = @key
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -9,3 +9,38 @@ BEGIN
|
|||||||
VALUES (@companyId, @type);
|
VALUES (@companyId, @type);
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIST_BY_COMPANY = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
[type],
|
||||||
|
prefix,
|
||||||
|
padding,
|
||||||
|
[next] AS [next]
|
||||||
|
FROM numbers
|
||||||
|
WHERE company_id = @companyId
|
||||||
|
ORDER BY [type]
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_BY_COMPANY_AND_ID = `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
[type],
|
||||||
|
prefix,
|
||||||
|
padding,
|
||||||
|
[next] AS [next]
|
||||||
|
FROM numbers
|
||||||
|
WHERE id = @id
|
||||||
|
AND company_id = @companyId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_BY_COMPANY_AND_ID = `
|
||||||
|
UPDATE numbers
|
||||||
|
SET
|
||||||
|
prefix = @prefix,
|
||||||
|
padding = @padding,
|
||||||
|
[next] = @next
|
||||||
|
OUTPUT INSERTED.id, INSERTED.[type], INSERTED.prefix, INSERTED.padding, INSERTED.[next]
|
||||||
|
WHERE id = @id
|
||||||
|
AND company_id = @companyId
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { DELETE_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js';
|
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
|
||||||
|
|
||||||
export default function del(pool) {
|
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a company setting by key for the user's selected company.
|
|
||||||
*
|
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
|
||||||
* @param {string} key - The setting key.
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
* @throws {Error} If the setting is not found.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* await store.companysetting.delete(42, 'theme');
|
|
||||||
*/
|
|
||||||
return async (googleAuthId, key) => {
|
|
||||||
if (!key?.trim()) {
|
|
||||||
throw new Error('Setting not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { companyId } = await access(googleAuthId);
|
|
||||||
|
|
||||||
const result = await pool
|
|
||||||
.request()
|
|
||||||
.input('companyId', companyId)
|
|
||||||
.input('key', key.trim())
|
|
||||||
.query(DELETE_BY_COMPANY_AND_KEY);
|
|
||||||
|
|
||||||
if (!result.rowsAffected[0]) {
|
|
||||||
throw new Error('Setting not found');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,27 +2,38 @@ import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../sql/companysetting.js';
|
|||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import shapeSetting from './shapeSetting.js';
|
import shapeSetting from './shapeSetting.js';
|
||||||
|
|
||||||
|
const ALLOWED_KEYS = new Set([
|
||||||
|
'ui.theme',
|
||||||
|
'keys.tavily',
|
||||||
|
'keys.openrouter',
|
||||||
|
'labelprinter.addr',
|
||||||
|
'labelprinter.type',
|
||||||
|
]);
|
||||||
|
|
||||||
export default function set(pool) {
|
export default function set(pool) {
|
||||||
const access = resolveSelectedCompanyAccess(pool);
|
const access = resolveSelectedCompanyAccess(pool);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets (upserts) a company setting by key for the user's selected company.
|
* Sets (upserts) a company setting by key for the user's selected company.
|
||||||
*
|
*
|
||||||
|
* Only a fixed allow-list of keys can be set: `ui.theme`, `keys.tavily`,
|
||||||
|
* `keys.openrouter`, `labelprinter.addr`, `labelprinter.type`.
|
||||||
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {object} input - The setting input.
|
* @param {object} input - The setting input.
|
||||||
* @param {string} input.key - The setting key.
|
* @param {string} input.key - The setting key (must be in the allow-list).
|
||||||
* @param {string} input.value - The setting value.
|
* @param {string} input.value - The setting value.
|
||||||
* @param {boolean} [input.frontend] - Whether the setting is frontend-visible.
|
* @param {boolean} [input.frontend] - Whether the setting is frontend-visible.
|
||||||
* @returns {Promise<{key: string, value: string, frontend: boolean}>} The upserted setting.
|
* @returns {Promise<{key: string, value: string, frontend: boolean}>} The upserted setting.
|
||||||
* @throws {Error} If the input is invalid.
|
* @throws {Error} If the input is invalid.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const setting = await store.companysetting.set(42, { key: 'theme', value: 'dark', frontend: true });
|
* const setting = await store.companysetting.set(42, { key: 'ui.theme', value: 'dark' });
|
||||||
* // => { key, value, frontend }
|
* // => { key, value, frontend }
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, input = {}) => {
|
return async (googleAuthId, input = {}) => {
|
||||||
const key = input.key?.trim();
|
const key = input.key?.trim();
|
||||||
if (!key) {
|
if (!ALLOWED_KEYS.has(key)) {
|
||||||
throw new Error('Invalid setting');
|
throw new Error('Invalid setting');
|
||||||
}
|
}
|
||||||
if (typeof input.value !== 'string') {
|
if (typeof input.value !== 'string') {
|
||||||
|
|||||||
37
src/store/numbers/list.js
Normal file
37
src/store/numbers/list.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { LIST_BY_COMPANY } from '../../sql/numbers.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
|
||||||
|
function shape(row) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
type: row.type,
|
||||||
|
prefix: row.prefix,
|
||||||
|
padding: row.padding,
|
||||||
|
next: row.next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function list(pool) {
|
||||||
|
const access = resolveSelectedCompanyAccess(pool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the number sequences of the user's selected company.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @returns {Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>} The number sequences.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const numbers = await store.numbers.list(42);
|
||||||
|
* // => [{ id, type, prefix, padding, next }]
|
||||||
|
*/
|
||||||
|
return async (googleAuthId) => {
|
||||||
|
const { companyId } = await access(googleAuthId);
|
||||||
|
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('companyId', companyId)
|
||||||
|
.query(LIST_BY_COMPANY);
|
||||||
|
|
||||||
|
return result.recordset.map(shape);
|
||||||
|
};
|
||||||
|
}
|
||||||
100
src/store/numbers/update.js
Normal file
100
src/store/numbers/update.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID,
|
||||||
|
UPDATE_BY_COMPANY_AND_ID,
|
||||||
|
} from '../../sql/numbers.js';
|
||||||
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
|
|
||||||
|
function shape(row) {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
type: row.type,
|
||||||
|
prefix: row.prefix,
|
||||||
|
padding: row.padding,
|
||||||
|
next: row.next,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function update(pool) {
|
||||||
|
const access = resolveSelectedCompanyAccess(pool);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the sequence settings of a number for the user's selected company.
|
||||||
|
*
|
||||||
|
* Only the provided fields are changed (all optional). `prefix` is trimmed and
|
||||||
|
* must be at most 50 characters. `padding` and `next` must be finite numbers
|
||||||
|
* greater than or equal to 0 and 1 respectively. If no field is provided, the
|
||||||
|
* row is returned unchanged.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} id - The id of the number sequence to update.
|
||||||
|
* @param {object} input - The update input.
|
||||||
|
* @param {string} [input.prefix] - The new prefix.
|
||||||
|
* @param {number} [input.padding] - The new padding length.
|
||||||
|
* @param {number} [input.next] - The new next value.
|
||||||
|
* @returns {Promise<{id: number, type: string, prefix: string, padding: number, next: number}>} The updated number sequence.
|
||||||
|
* @throws {Error} If the number is not found or a field is invalid.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const number = await store.numbers.update(42, 3, { prefix: 'A-', padding: 4, next: 100 });
|
||||||
|
* // => { id, type, prefix, padding, next }
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, id, input = {}) => {
|
||||||
|
if (!Number.isFinite(id)) {
|
||||||
|
throw new Error('Number not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { companyId } = await access(googleAuthId);
|
||||||
|
|
||||||
|
const existing = await pool
|
||||||
|
.request()
|
||||||
|
.input('id', id)
|
||||||
|
.input('companyId', companyId)
|
||||||
|
.query(GET_BY_COMPANY_AND_ID);
|
||||||
|
|
||||||
|
const current = existing.recordset[0];
|
||||||
|
if (!current) {
|
||||||
|
throw new Error('Number not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = { ...current };
|
||||||
|
|
||||||
|
if ('prefix' in input) {
|
||||||
|
const prefix = input.prefix == null ? '' : String(input.prefix).trim();
|
||||||
|
if (prefix.length > 50) {
|
||||||
|
throw new Error('Invalid number');
|
||||||
|
}
|
||||||
|
patch.prefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('padding' in input) {
|
||||||
|
const padding = input.padding;
|
||||||
|
if (!Number.isInteger(padding) || padding < 0) {
|
||||||
|
throw new Error('Invalid number');
|
||||||
|
}
|
||||||
|
patch.padding = padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('next' in input) {
|
||||||
|
const next = input.next;
|
||||||
|
if (!Number.isInteger(next) || next < 1) {
|
||||||
|
throw new Error('Invalid number');
|
||||||
|
}
|
||||||
|
patch.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('id', id)
|
||||||
|
.input('companyId', companyId)
|
||||||
|
.input('prefix', patch.prefix)
|
||||||
|
.input('padding', patch.padding)
|
||||||
|
.input('next', patch.next)
|
||||||
|
.query(UPDATE_BY_COMPANY_AND_ID);
|
||||||
|
|
||||||
|
const row = result.recordset[0];
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Number not found');
|
||||||
|
}
|
||||||
|
return shape(row);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user