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

@@ -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;