file meta

This commit is contained in:
seb
2026-09-18 05:19:47 +02:00
parent e10a8d10e7
commit 9e9d3154ca
17 changed files with 896 additions and 623 deletions

View File

@@ -2,6 +2,12 @@
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `delivery.attachments`
*Source: `src/store/delivery/attachments.js`*
Loads attachment metadata (without binary content) for a manifest.
## `delivery.create`
*Source: `src/store/delivery/create.js`*

View File

@@ -26,10 +26,11 @@ Attaches a file to an existing manifest.
| `[input.thumbavif]` | `Buffer` | Optional AVIF thumbnail buffer. |
| `[input.thumbx]` | `number` | Optional thumbnail width. |
| `[input.thumby]` | `number` | Optional thumbnail height. |
| `[input.meta]` | `object` | Optional string key/value metadata stored on the file row. |
**Returns**
`Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>` — The created attachment record.
`Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>` — The created attachment record.
**Throws**
@@ -62,7 +63,7 @@ Gets an attachment for a manifest by its attachment ID or file ID, including ful
**Returns**
`Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>` — The attachment record with binary content.
`Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>` — The attachment record with binary content.
**Throws**
@@ -79,29 +80,7 @@ const attachment = await store.manifestattachment.getById(42, 1, 5);
*Source: `src/store/manifestattachment/listByManifestId.js`*
Lists all attachments for a given manifest, without loading their full binary content.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `manifestId` | `number` | The manifest ID. |
**Returns**
`Promise<Array<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>>` — A list of attachment metadata objects.
**Throws**
`Error` — If the manifest is not found or the user is not authorized.
**Example**
```js
const attachments = await store.manifestattachment.listByManifestId(42, 1);
// => [{ id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby }]
```
Parse the file.meta JSON text column into an object (string key/value).
## `manifestattachment.order`

9
API.md
View File

@@ -30,7 +30,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0`
- **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0`
- **entityhistory**: `id PK, entity_id->entity, companyuser_id?->companyuser, date:dt, text:txt`
- **file**: `id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i`
- **file**: `id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt`
- **google_auth**: `id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company`
- **language**: `id PK, iso639:s(10) UQ`
- **ledger**: `id PK, manifestarticle_id->manifestarticle, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]`
@@ -152,6 +152,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `order(googleAuthId: number, sku: string, input: {ids: Array<number>}): Promise<{ids: Array<number>}>`
- `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>`
- [delivery](API.delivery.md)
- `attachments(): void`
- `create(googleAuthId: number, body: {date: string, supplierId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `delete(googleAuthId: number, id: number): Promise<void>`
- `getById(googleAuthId: number, id: number): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
@@ -170,9 +171,9 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `list(googleAuthId: number): Promise<Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string}>>`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- [manifestattachment](API.manifestattachment.md)
- `add(googleAuthId: number, manifestId: number, input: {mimetype: string, name: string, content: Buffer, sort?: number, size?: number, created?: Date|string, modified?: Date|string, hash?: string, thumbavif?: Buffer, thumbx?: number, thumby?: number}): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>`
- `getById(googleAuthId: number, manifestId: number, id: number): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>`
- `listByManifestId(googleAuthId: number, manifestId: number): Promise<Array<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>>`
- `add(googleAuthId: number, manifestId: number, input: {mimetype: string, name: string, content: Buffer, sort?: number, size?: number, created?: Date|string, modified?: Date|string, hash?: string, thumbavif?: Buffer, thumbx?: number, thumby?: number, meta?: object}): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>`
- `getById(googleAuthId: number, manifestId: number, id: number): Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>`
- `listByManifestId(): void`
- `order(googleAuthId: number, manifestId: number, input: {ids?: Array<number>}): Promise<{ids: Array<number>}>`
- `remove(googleAuthId: number, manifestId: number, id: number): Promise<void>`
- [posterminal](API.posterminal.md)

View File

@@ -331,7 +331,7 @@ const SCHEMA_TABLES = [
['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'],
['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'],
['entityhistory', 'id PK, entity_id->entity, companyuser_id?->companyuser, date:dt, text:txt'],
['file', 'id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i'],
['file', 'id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt'],
['google_auth', 'id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company'],
['language', 'id PK, iso639:s(10) UQ'],
['ledger', 'id PK, manifestarticle_id->manifestarticle, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]'],

View File

@@ -55,7 +55,7 @@ digraph schema {
pic [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="2" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD><TD PORT="in1" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD><TD PORT="in2" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="3" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>pic</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="w" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">w</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="h" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">h</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="size" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">size</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">BIGINT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="hash" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">hash</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(64)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbw" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbw</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbh" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbh</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbhash" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbhash</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(64)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="source" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">source</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="fullbinary" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">fullbinary</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbbinary" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbbinary</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
brandpic [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="2" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>brandpic</B></FONT></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="brand_id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>brand_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="pic_id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>pic_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="sort" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">sort</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
articlepic [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="2" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="4" BORDER="1"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>articlepic</B></FONT></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="article_id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>article_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="pic_id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>pic_id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>FK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="sort" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">sort</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
file [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="2" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>file</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="hash" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">hash</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(64)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="mimetype" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">mimetype</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="name" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">name</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="size" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">size</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">BIGINT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="created" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">created</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">DATETIME2</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="modified" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">modified</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">DATETIME2</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="content" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">content</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbavif" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbavif?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbx" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbx?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumby" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumby?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
file [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4" COLOR="#3f4c5f"><TR><TD COLSPAN="2" BGCOLOR="#33415c" ALIGN="CENTER" CELLPADDING="1" BORDER="1"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD PORT="in0" BGCOLOR="#33415c" BORDER="0" WIDTH="10" HEIGHT="4" CELLPADDING="0"></TD></TR><TR><TD COLSPAN="1" ALIGN="CENTER" CELLPADDING="3"><FONT COLOR="#e2e8f0" POINT-SIZE="12"><B>file</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="id" COLSPAN="2" BGCOLOR="#2b3a52" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9"><B>id</B></FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT <B>PK</B></FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="hash" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">hash</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(64)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="mimetype" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">mimetype</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="name" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">name</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(255)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="size" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">size</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">BIGINT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="created" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">created</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">DATETIME2</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="modified" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">modified</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">DATETIME2</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="content" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">content</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbavif" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbavif?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">VARBINARY(MAX)</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumbx" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumbx?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="thumby" COLSPAN="2" BGCOLOR="#2a3242" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">thumby?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">INT</FONT></TD></TR></TABLE></TD></TR><TR><TD PORT="meta" COLSPAN="2" BGCOLOR="#272e3d" BORDER="1" CELLPADDING="0"><TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" FIXEDSIZE="FALSE"><TR><TD ALIGN="LEFT" CELLPADDING="4"><FONT COLOR="#d8dee9">meta?</FONT></TD><TD ALIGN="RIGHT" CELLPADDING="4"><FONT COLOR="#8b95a7" POINT-SIZE="9">NVARCHAR(MAX)</FONT></TD></TR></TABLE></TD></TR></TABLE>>];
}
subgraph cluster_manifests {
label="Documents"; labeljust="l"; fontsize=15;
@@ -93,7 +93,7 @@ digraph schema {
category:parent_id:e -> category:id:e [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_category_parent", constraint=false];
company:root_category_id:w -> category:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3];
company:default_language_id:se -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1];
company:taxrate:e -> tax:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1];
company:taxrate:e -> tax:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1];
company:reducedtaxrate:e -> tax:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1];
company:taxfreerate:e -> tax:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1];
customergroup:company_id:w -> company:in2:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3];
@@ -131,7 +131,7 @@ digraph schema {
categorytext:language_id:nw -> language:in3:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_language", constraint=false, weight=1];
articletext:article_id:w -> article:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_articletext_article", weight=3];
articletext:language_id:w -> language:in2:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_articletext_language", constraint=false, weight=1];
price:tax_id:w -> tax:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_price_tax", constraint=false, weight=1];
price:tax_id:w -> tax:in3:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_price_tax", constraint=false, weight=1];
price:article_id:e -> article:in7:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_article", weight=3];
price:customergroup_id:e -> customergroup:in2:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_customergroup", constraint=false, weight=1];
articlesupplier:article_id:sw -> article:in5:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_articlesupplier_article", weight=3];

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -782,7 +782,8 @@ BEGIN
content VARBINARY(MAX) NOT NULL,
thumbavif VARBINARY(MAX) NULL,
thumbx INT NULL,
thumby INT NULL
thumby INT NULL,
meta NVARCHAR(MAX) NULL
);
END
`;

View File

@@ -74,28 +74,44 @@ DELETE FROM manifestarticle
WHERE manifest_id = @manifestId
`;
export const CREATE_LINE = `
INSERT INTO manifestarticle (
manifest_id,
sku,
name,
pu,
amount,
net,
comment,
rebate,
taxrate
)
OUTPUT INSERTED.id
VALUES (
@manifestId,
@sku,
@name,
@pu,
@amount,
@net,
@comment,
@rebate,
@taxrate
)
export const GET_NEXT_ATTACHMENT_SORT = `
SELECT ISNULL(MAX(ma.sort), -1) + 1 AS sort
FROM manifestattachment ma
WHERE ma.manifest_id = @manifestId
`;
export const CREATE_FILE = `
INSERT INTO [file] (hash, mimetype, name, size, created, modified, content, thumbavif, thumbx, thumby, meta)
OUTPUT INSERTED.id
VALUES (@hash, @mimetype, @name, @size, @created, @modified, @content, CONVERT(VARBINARY(MAX), @thumbavif), @thumbx, @thumby, @meta)
`;
export const CREATE_ATTACHMENT = `
INSERT INTO manifestattachment (manifest_id, file_id, sort)
OUTPUT INSERTED.id
VALUES (@manifestId, @fileId, @sort)
`;
export const LIST_DELIVERY_FILES = `
SELECT
ma.id,
ma.manifest_id AS manifestId,
ma.file_id AS fileId,
ma.sort,
f.hash,
f.mimetype,
f.name,
f.size,
f.created,
f.modified,
f.thumbavif,
f.thumbx,
f.thumby,
f.meta
FROM manifestattachment ma
INNER JOIN [file] f ON f.id = ma.file_id
INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE m.company_id = @companyId
AND m.[type] = @type
ORDER BY ma.manifest_id, ma.sort ASC, ma.id ASC
`;

View File

@@ -5,9 +5,9 @@ WHERE id = @manifestId AND company_id = @companyId
`;
export const CREATE_FILE = `
INSERT INTO [file] (hash, mimetype, name, size, created, modified, content, thumbavif, thumbx, thumby)
INSERT INTO [file] (hash, mimetype, name, size, created, modified, content, thumbavif, thumbx, thumby, meta)
OUTPUT INSERTED.id
VALUES (@hash, @mimetype, @name, @size, @created, @modified, @content, CONVERT(VARBINARY(MAX), @thumbavif), @thumbx, @thumby)
VALUES (@hash, @mimetype, @name, @size, @created, @modified, @content, CONVERT(VARBINARY(MAX), @thumbavif), @thumbx, @thumby, @meta)
`;
export const GET_NEXT_SORT = `
@@ -36,7 +36,8 @@ SELECT
f.modified,
f.thumbavif,
f.thumbx,
f.thumby
f.thumby,
f.meta
FROM manifestattachment ma
INNER JOIN [file] f ON f.id = ma.file_id
WHERE ma.manifest_id = @manifestId
@@ -58,7 +59,8 @@ SELECT
f.content,
f.thumbavif,
f.thumbx,
f.thumby
f.thumby,
f.meta
FROM manifestattachment ma
INNER JOIN [file] f ON f.id = ma.file_id
WHERE ma.manifest_id = @manifestId

View File

@@ -0,0 +1,172 @@
import { createHash } from 'node:crypto';
import { LIST_BY_MANIFEST } from '../../sql/manifestattachment.js';
import request from '../request.js';
import { parseMeta } from '../manifestattachment/listByManifestId.js';
const ALLOWED_MIMETYPES = new Set([
'application/pdf',
'text/plain',
]);
export function shapeAttachmentMeta(row) {
return {
id: row.id,
manifestId: row.manifestId,
fileId: row.fileId,
sort: row.sort,
hash: row.hash,
mimetype: row.mimetype,
name: row.name,
size: Number(row.size),
created: row.created,
modified: row.modified,
thumbavif: row.thumbavif ?? null,
thumbx: row.thumbx ?? null,
thumby: row.thumby ?? null,
meta: parseMeta(row.meta),
};
}
/**
* Loads attachment metadata (without binary content) for a manifest.
*/
export async function loadAttachments(pool, tx, manifestId) {
const result = await request(pool, tx)
.input('manifestId', manifestId)
.query(LIST_BY_MANIFEST);
return result.recordset.map(shapeAttachmentMeta);
}
function normalizeAttachment(raw, index) {
if (typeof raw !== 'object' || raw === null) {
throw new Error('Invalid file');
}
const mimetype =
typeof raw.mimetype === 'string' ? raw.mimetype.trim().toLowerCase() : '';
if (!ALLOWED_MIMETYPES.has(mimetype)) {
throw new Error('Invalid file');
}
const name = typeof raw.name === 'string' ? raw.name.trim() : '';
if (!name) {
throw new Error('Invalid file');
}
if (raw.content == null) {
throw new Error('Invalid file');
}
const content = Buffer.isBuffer(raw.content)
? raw.content
: Buffer.from(raw.content);
const size = Number.isFinite(raw.size) && raw.size >= 0
? Number(raw.size)
: content.length;
const hash =
typeof raw.hash === 'string' && raw.hash.trim()
? raw.hash.trim()
: createHash('sha256').update(content).digest('hex');
const parseDate = (value) => {
if (value instanceof Date && !isNaN(value)) return value;
if (typeof value === 'string' || typeof value === 'number') {
const parsed = new Date(value);
return isNaN(parsed) ? null : parsed;
}
return null;
};
const created = parseDate(raw.created ?? raw.createdAt) ?? new Date();
const modified = parseDate(raw.modified ?? raw.modifiedAt) ?? created;
let meta = null;
if (typeof raw.meta === 'object' && raw.meta !== null && !Array.isArray(raw.meta)) {
meta = {};
for (const [key, value] of Object.entries(raw.meta)) {
if (value === undefined || value === null) continue;
meta[key] = String(value);
}
if (Object.keys(meta).length === 0) {
meta = null;
}
}
return {
mimetype,
name,
content,
size,
hash,
created,
modified,
meta,
sort: Number.isFinite(raw.sort) ? Number(raw.sort) : null,
};
}
/**
* Inserts pdf/txt attachments for a delivery (manifest) inside an existing
* transaction. Sort orders continue after the current maximum.
*
* @param {object} pool
* @param {object} tx - Active transaction.
* @param {object} sql - SQL statements from delivery.js.
* @param {number} manifestId
* @param {Array<{mimetype: string, name: string, content: Buffer, sort?: number, size?: number, created?: Date|string, modified?: Date|string, hash?: string}>} [files]
* @returns {Promise<Array<object>>} Attachment metadata rows.
*/
export async function insertAttachments(pool, tx, sql, manifestId, files) {
const attachments = (files ?? []).map(normalizeAttachment);
const sortRes = await request(pool, tx)
.input('manifestId', manifestId)
.query(sql.GET_NEXT_ATTACHMENT_SORT);
let nextSort = sortRes.recordset[0].sort;
const results = [];
for (const a of attachments) {
const sort = a.sort ?? nextSort;
nextSort = Math.max(nextSort, sort) + 1;
const fileRes = await request(pool, tx)
.input('hash', a.hash)
.input('mimetype', a.mimetype)
.input('name', a.name)
.input('size', a.size)
.input('created', a.created)
.input('modified', a.modified)
.input('content', a.content)
.input('thumbavif', null)
.input('thumbx', null)
.input('thumby', null)
.input('meta', a.meta === null ? null : JSON.stringify(a.meta))
.query(sql.CREATE_FILE);
const fileId = fileRes.recordset[0].id;
await request(pool, tx)
.input('manifestId', manifestId)
.input('fileId', fileId)
.input('sort', sort)
.query(sql.CREATE_ATTACHMENT);
results.push({
manifestId,
fileId,
sort,
hash: a.hash,
mimetype: a.mimetype,
name: a.name,
size: a.size,
created: a.created,
modified: a.modified,
meta: a.meta,
});
}
return results;
}

View File

@@ -1,10 +1,17 @@
import { CREATE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js';
import {
CREATE_MANIFEST,
CREATE_FILE,
CREATE_ATTACHMENT,
GET_NEXT_ATTACHMENT_SORT,
DELIVERY_TYPE,
} from '../../sql/delivery.js';
import request from '../request.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import nextNumber from '../numbers/nextNumber.js';
import taxEnsure from '../tax/ensure.js';
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
import { loadDelivery } from './helpers.js';
import { insertAttachments } from './attachments.js';
import { assertSupplier } from './writeLines.js';
export default function create(pool) {
@@ -73,6 +80,18 @@ export default function create(pool) {
ensureTax,
});
await insertAttachments(
pool,
tx,
{
GET_NEXT_ATTACHMENT_SORT,
CREATE_FILE,
CREATE_ATTACHMENT,
},
manifestId,
body.files,
);
const delivery = await loadDelivery(pool, tx, companyId, manifestId);
await tx.commit();
return delivery;

View File

@@ -5,6 +5,7 @@ import {
} from '../../sql/delivery.js';
import request from '../request.js';
import { shapeLine } from '../manifestLines.js';
import { loadAttachments } from './attachments.js';
export function shapeSummary(row) {
return {
@@ -33,8 +34,11 @@ export async function loadDelivery(pool, tx, companyId, id) {
.input('manifestId', id)
.query(GET_DELIVERY_LINES);
const files = await loadAttachments(pool, tx, id);
return {
...shapeSummary(row),
lines: lines.recordset.map(shapeLine),
files,
};
}

View File

@@ -1,6 +1,8 @@
import { DELIVERY_TYPE, LIST_DELIVERIES } from '../../sql/delivery.js';
import { DELIVERY_TYPE, LIST_DELIVERIES, LIST_DELIVERY_FILES } from '../../sql/delivery.js';
import request from '../request.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import { shapeSummary } from './helpers.js';
import { shapeAttachmentMeta } from './attachments.js';
export default function list(pool) {
const access = resolveSelectedCompanyAccess(pool);
@@ -24,6 +26,22 @@ export default function list(pool) {
.input('type', DELIVERY_TYPE)
.query(LIST_DELIVERIES);
return result.recordset.map(shapeSummary);
const filesResult = await pool
.request()
.input('companyId', companyId)
.input('type', DELIVERY_TYPE)
.query(LIST_DELIVERY_FILES);
const filesByManifest = new Map();
for (const row of filesResult.recordset) {
const list = filesByManifest.get(row.manifestId) ?? [];
list.push(shapeAttachmentMeta(row));
filesByManifest.set(row.manifestId, list);
}
return result.recordset.map((row) => ({
...shapeSummary(row),
files: filesByManifest.get(row.id) ?? [],
}));
};
}

View File

@@ -2,6 +2,9 @@ import {
DELETE_LINES_BY_MANIFEST,
DELIVERY_TYPE,
GET_DELIVERY_HEADER,
GET_NEXT_ATTACHMENT_SORT,
CREATE_FILE,
CREATE_ATTACHMENT,
UPDATE_MANIFEST,
} from '../../sql/delivery.js';
import request from '../request.js';
@@ -9,6 +12,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import taxEnsure from '../tax/ensure.js';
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
import { loadDelivery, shapeSummary } from './helpers.js';
import { insertAttachments } from './attachments.js';
import { assertSupplier } from './writeLines.js';
export default function update(pool) {
@@ -46,8 +50,9 @@ export default function update(pool) {
);
const hasExternalNumber = Object.prototype.hasOwnProperty.call(patch, 'externalNumber');
const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines');
const hasFiles = Object.prototype.hasOwnProperty.call(patch, 'files');
if (!hasNumber && !hasDate && !hasSupplierId && !hasExternalNumber && !hasLines) {
if (!hasNumber && !hasDate && !hasSupplierId && !hasExternalNumber && !hasLines && !hasFiles) {
throw new Error('Invalid delivery');
}
@@ -130,6 +135,20 @@ export default function update(pool) {
});
}
if (hasFiles) {
await insertAttachments(
pool,
tx,
{
GET_NEXT_ATTACHMENT_SORT,
CREATE_FILE,
CREATE_ATTACHMENT,
},
id,
patch.files,
);
}
const delivery = await loadDelivery(pool, tx, companyId, id);
await tx.commit();
return delivery;

View File

@@ -30,7 +30,8 @@ export default function add(pool) {
* @param {Buffer} [input.thumbavif] - Optional AVIF thumbnail buffer.
* @param {number} [input.thumbx] - Optional thumbnail width.
* @param {number} [input.thumby] - Optional thumbnail height.
* @returns {Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>} The created attachment record.
* @param {object} [input.meta] - Optional string key/value metadata stored on the file row.
* @returns {Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>} The created attachment record.
* @throws {Error} If the manifest is not found or file input is invalid.
*
* @example
@@ -42,7 +43,7 @@ export default function add(pool) {
* // => { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby }
*/
return async (googleAuthId, manifestId, input, ...rest) => {
let rawMimetype, rawName, rawContent, rawHash, rawThumbavif, rawThumbx, rawThumby, rawSort, rawSize, rawCreated, rawModified;
let rawMimetype, rawName, rawContent, rawHash, rawThumbavif, rawThumbx, rawThumby, rawSort, rawSize, rawCreated, rawModified, rawMeta;
if (typeof input === 'object' && input !== null && !Buffer.isBuffer(input)) {
rawMimetype = input.mimetype;
rawName = input.name;
@@ -55,6 +56,7 @@ export default function add(pool) {
rawSize = input.size;
rawCreated = input.created ?? input.createdAt;
rawModified = input.modified ?? input.modifiedAt ?? input.lastModified;
rawMeta = input.meta;
} else {
rawMimetype = input;
rawName = rest[0];
@@ -105,6 +107,19 @@ export default function add(pool) {
: null;
const modified = parsedModified && !isNaN(parsedModified) ? parsedModified : created;
// Simple string key/value metadata; only plain string values are kept.
let meta = null;
if (typeof rawMeta === 'object' && rawMeta !== null && !Array.isArray(rawMeta)) {
meta = {};
for (const [key, value] of Object.entries(rawMeta)) {
if (value === undefined || value === null) continue;
meta[key] = String(value);
}
if (Object.keys(meta).length === 0) {
meta = null;
}
}
const hash =
typeof rawHash === 'string' && rawHash.trim()
? rawHash.trim()
@@ -168,6 +183,7 @@ export default function add(pool) {
.input('thumbavif', thumbavif)
.input('thumbx', thumbx)
.input('thumby', thumby)
.input('meta', meta === null ? null : JSON.stringify(meta))
.query(CREATE_FILE);
const fileId = fileRes.recordset[0].id;
@@ -196,6 +212,7 @@ export default function add(pool) {
thumbavif,
thumbx,
thumby,
meta,
};
} catch (err) {
try {

View File

@@ -1,6 +1,7 @@
import { GET_BY_ID } from '../../sql/manifestattachment.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import resolveManifestAccess from './resolveManifestAccess.js';
import { parseMeta } from './listByManifestId.js';
import request from '../request.js';
export default function getById(pool) {
@@ -13,7 +14,7 @@ export default function getById(pool) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} manifestId - The manifest ID.
* @param {number} id - The attachment ID or file ID.
* @returns {Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>} The attachment record with binary content.
* @returns {Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>} The attachment record with binary content.
* @throws {Error} If the manifest or attachment is not found, or user is not authorized.
*
* @example
@@ -53,6 +54,7 @@ export default function getById(pool) {
thumbavif: row.thumbavif ?? null,
thumbx: row.thumbx ?? null,
thumby: row.thumby ?? null,
meta: parseMeta(row.meta),
};
};
}

View File

@@ -3,6 +3,18 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import resolveManifestAccess from './resolveManifestAccess.js';
import request from '../request.js';
/** Parse the file.meta JSON text column into an object (string key/value). */
export function parseMeta(raw) {
if (raw == null) return null;
if (typeof raw === 'object') return raw;
try {
const parsed = JSON.parse(raw);
return typeof parsed === 'object' && parsed !== null ? parsed : null;
} catch {
return null;
}
}
function shape(row) {
return {
id: row.id,
@@ -18,6 +30,7 @@ function shape(row) {
thumbavif: row.thumbavif ?? null,
thumbx: row.thumbx ?? null,
thumby: row.thumby ?? null,
meta: parseMeta(row.meta),
};
}
@@ -30,7 +43,7 @@ export default function listByManifestId(pool) {
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} manifestId - The manifest ID.
* @returns {Promise<Array<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>>} A list of attachment metadata objects.
* @returns {Promise<Array<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, size: number, created: Date, modified: Date, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null, meta: object|null}>>} A list of attachment metadata objects.
* @throws {Error} If the manifest is not found or the user is not authorized.
*
* @example