feat: add hash and thumbnail columns to file, sort column and order method to manifestattachment
This commit is contained in:
@@ -119,6 +119,7 @@ import manifestattachmentAdd from './store/manifestattachment/add.js';
|
||||
import manifestattachmentRemove from './store/manifestattachment/remove.js';
|
||||
import manifestattachmentListByManifestId from './store/manifestattachment/listByManifestId.js';
|
||||
import manifestattachmentGetById from './store/manifestattachment/getById.js';
|
||||
import manifestattachmentOrder from './store/manifestattachment/order.js';
|
||||
|
||||
export default class LibDashStore {
|
||||
constructor(pool) {
|
||||
@@ -310,6 +311,7 @@ export default class LibDashStore {
|
||||
remove: manifestattachmentRemove(pool),
|
||||
listByManifestId: manifestattachmentListByManifestId(pool),
|
||||
getById: manifestattachmentGetById(pool),
|
||||
order: manifestattachmentOrder(pool),
|
||||
};
|
||||
|
||||
this.purge = storePurge(pool);
|
||||
|
||||
@@ -773,9 +773,13 @@ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file')
|
||||
BEGIN
|
||||
CREATE TABLE [file] (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
hash NVARCHAR(64) NOT NULL,
|
||||
mimetype NVARCHAR(255) NOT NULL,
|
||||
name NVARCHAR(255) NOT NULL,
|
||||
content VARBINARY(MAX) NOT NULL
|
||||
content VARBINARY(MAX) NOT NULL,
|
||||
thumbavif VARBINARY(MAX) NULL,
|
||||
thumbx INT NULL,
|
||||
thumby INT NULL
|
||||
);
|
||||
END
|
||||
`;
|
||||
@@ -787,8 +791,10 @@ BEGIN
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
manifest_id INT NOT NULL,
|
||||
file_id INT NOT NULL,
|
||||
sort INT NOT NULL CONSTRAINT DF_manifestattachment_sort DEFAULT 0,
|
||||
CONSTRAINT FK_manifestattachment_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_manifestattachment_file FOREIGN KEY (file_id) REFERENCES [file](id) ON DELETE CASCADE
|
||||
CONSTRAINT FK_manifestattachment_file FOREIGN KEY (file_id) REFERENCES [file](id) ON DELETE CASCADE,
|
||||
CONSTRAINT UQ_manifestattachment_manifest_sort UNIQUE (manifest_id, sort)
|
||||
);
|
||||
END
|
||||
`;
|
||||
|
||||
@@ -5,15 +5,21 @@ WHERE id = @manifestId AND company_id = @companyId
|
||||
`;
|
||||
|
||||
export const CREATE_FILE = `
|
||||
INSERT INTO [file] (mimetype, name, content)
|
||||
INSERT INTO [file] (hash, mimetype, name, content, thumbavif, thumbx, thumby)
|
||||
OUTPUT INSERTED.id
|
||||
VALUES (@mimetype, @name, @content)
|
||||
VALUES (@hash, @mimetype, @name, @content, CONVERT(VARBINARY(MAX), @thumbavif), @thumbx, @thumby)
|
||||
`;
|
||||
|
||||
export const GET_NEXT_SORT = `
|
||||
SELECT ISNULL(MAX(sort), -1) + 1 AS sort
|
||||
FROM manifestattachment
|
||||
WHERE manifest_id = @manifestId;
|
||||
`;
|
||||
|
||||
export const CREATE_ATTACHMENT = `
|
||||
INSERT INTO manifestattachment (manifest_id, file_id)
|
||||
INSERT INTO manifestattachment (manifest_id, file_id, sort)
|
||||
OUTPUT INSERTED.id
|
||||
VALUES (@manifestId, @fileId)
|
||||
VALUES (@manifestId, @fileId, @sort)
|
||||
`;
|
||||
|
||||
export const LIST_BY_MANIFEST = `
|
||||
@@ -21,13 +27,18 @@ SELECT
|
||||
ma.id,
|
||||
ma.manifest_id AS manifestId,
|
||||
ma.file_id AS fileId,
|
||||
ma.sort,
|
||||
f.hash,
|
||||
f.mimetype,
|
||||
f.name,
|
||||
DATALENGTH(f.content) AS size
|
||||
DATALENGTH(f.content) AS size,
|
||||
f.thumbavif,
|
||||
f.thumbx,
|
||||
f.thumby
|
||||
FROM manifestattachment ma
|
||||
INNER JOIN [file] f ON f.id = ma.file_id
|
||||
WHERE ma.manifest_id = @manifestId
|
||||
ORDER BY ma.id ASC
|
||||
ORDER BY ma.sort ASC, ma.id ASC
|
||||
`;
|
||||
|
||||
export const GET_BY_ID = `
|
||||
@@ -35,9 +46,14 @@ SELECT
|
||||
ma.id,
|
||||
ma.manifest_id AS manifestId,
|
||||
ma.file_id AS fileId,
|
||||
ma.sort,
|
||||
f.hash,
|
||||
f.mimetype,
|
||||
f.name,
|
||||
f.content
|
||||
f.content,
|
||||
f.thumbavif,
|
||||
f.thumbx,
|
||||
f.thumby
|
||||
FROM manifestattachment ma
|
||||
INNER JOIN [file] f ON f.id = ma.file_id
|
||||
WHERE ma.manifest_id = @manifestId
|
||||
@@ -60,3 +76,22 @@ export const DELETE_FILE = `
|
||||
DELETE FROM [file]
|
||||
WHERE id = @fileId
|
||||
`;
|
||||
|
||||
export const LIST_IDS_BY_MANIFEST = `
|
||||
SELECT id
|
||||
FROM manifestattachment
|
||||
WHERE manifest_id = @manifestId
|
||||
ORDER BY sort ASC, id ASC;
|
||||
`;
|
||||
|
||||
export const SET_TEMP_SORTS = `
|
||||
UPDATE manifestattachment
|
||||
SET sort = -id
|
||||
WHERE manifest_id = @manifestId;
|
||||
`;
|
||||
|
||||
export const UPDATE_SORT = `
|
||||
UPDATE manifestattachment
|
||||
SET sort = @sort
|
||||
WHERE id = @id AND manifest_id = @manifestId;
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { CREATE_FILE, CREATE_ATTACHMENT } from '../../sql/manifestattachment.js';
|
||||
import { createHash } from 'node:crypto';
|
||||
import sharp from 'sharp';
|
||||
import {
|
||||
CREATE_FILE,
|
||||
CREATE_ATTACHMENT,
|
||||
GET_NEXT_SORT,
|
||||
} from '../../sql/manifestattachment.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import resolveManifestAccess from './resolveManifestAccess.js';
|
||||
import request from '../request.js';
|
||||
@@ -16,7 +22,12 @@ export default function add(pool) {
|
||||
* @param {string} input.mimetype - The MIME type of the file (e.g. `'application/pdf'`).
|
||||
* @param {string} input.name - The file name.
|
||||
* @param {Buffer} input.content - The file content.
|
||||
* @returns {Promise<{id: number, manifestId: number, fileId: number, mimetype: string, name: string}>} The created attachment record.
|
||||
* @param {number} [input.sort] - Optional display sort order.
|
||||
* @param {string} [input.hash] - The file hash (auto-computed SHA-256 if omitted).
|
||||
* @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, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>} The created attachment record.
|
||||
* @throws {Error} If the manifest is not found or file input is invalid.
|
||||
*
|
||||
* @example
|
||||
@@ -25,14 +36,19 @@ export default function add(pool) {
|
||||
* name: 'invoice.pdf',
|
||||
* content: fileBuffer,
|
||||
* });
|
||||
* // => { id, manifestId, fileId, mimetype, name }
|
||||
* // => { id, manifestId, fileId, sort, hash, mimetype, name, thumbavif, thumbx, thumby }
|
||||
*/
|
||||
return async (googleAuthId, manifestId, input, ...rest) => {
|
||||
let rawMimetype, rawName, rawContent;
|
||||
let rawMimetype, rawName, rawContent, rawHash, rawThumbavif, rawThumbx, rawThumby, rawSort;
|
||||
if (typeof input === 'object' && input !== null && !Buffer.isBuffer(input)) {
|
||||
rawMimetype = input.mimetype;
|
||||
rawName = input.name;
|
||||
rawContent = input.content;
|
||||
rawHash = input.hash;
|
||||
rawThumbavif = input.thumbavif;
|
||||
rawThumbx = input.thumbx ?? input.thumbw;
|
||||
rawThumby = input.thumby ?? input.thumbh;
|
||||
rawSort = input.sort;
|
||||
} else {
|
||||
rawMimetype = input;
|
||||
rawName = rest[0];
|
||||
@@ -56,6 +72,49 @@ export default function add(pool) {
|
||||
? rawContent
|
||||
: Buffer.from(rawContent);
|
||||
|
||||
if (rawSort !== undefined && rawSort !== null) {
|
||||
if (!Number.isFinite(rawSort)) {
|
||||
throw new Error('Invalid sort');
|
||||
}
|
||||
}
|
||||
|
||||
const hash =
|
||||
typeof rawHash === 'string' && rawHash.trim()
|
||||
? rawHash.trim()
|
||||
: createHash('sha256').update(content).digest('hex');
|
||||
|
||||
let thumbavif = rawThumbavif
|
||||
? (Buffer.isBuffer(rawThumbavif) ? rawThumbavif : Buffer.from(rawThumbavif))
|
||||
: null;
|
||||
let thumbx = Number.isFinite(rawThumbx) ? Number(rawThumbx) : null;
|
||||
let thumby = Number.isFinite(rawThumby) ? Number(rawThumby) : null;
|
||||
|
||||
if (thumbavif && (!thumbx || !thumby)) {
|
||||
try {
|
||||
const meta = await sharp(thumbavif).metadata();
|
||||
thumbx = thumbx ?? meta.width ?? null;
|
||||
thumby = thumby ?? meta.height ?? null;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!thumbavif) {
|
||||
try {
|
||||
const thumbResult = await sharp(content)
|
||||
.resize(200, 200, { fit: 'inside', withoutEnlargement: true })
|
||||
.avif({ effort: 4 })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
thumbavif = thumbResult.data;
|
||||
thumbx = thumbResult.info.width;
|
||||
thumby = thumbResult.info.height;
|
||||
} catch {
|
||||
thumbavif = null;
|
||||
thumbx = null;
|
||||
thumby = null;
|
||||
}
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
await manifestAccess(companyId, manifestId);
|
||||
|
||||
@@ -63,10 +122,22 @@ export default function add(pool) {
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
let sort = rawSort;
|
||||
if (sort === undefined || sort === null) {
|
||||
const sortRes = await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.query(GET_NEXT_SORT);
|
||||
sort = sortRes.recordset[0].sort;
|
||||
}
|
||||
|
||||
const fileRes = await request(pool, tx)
|
||||
.input('hash', hash)
|
||||
.input('mimetype', mimetype)
|
||||
.input('name', name)
|
||||
.input('content', content)
|
||||
.input('thumbavif', thumbavif)
|
||||
.input('thumbx', thumbx)
|
||||
.input('thumby', thumby)
|
||||
.query(CREATE_FILE);
|
||||
|
||||
const fileId = fileRes.recordset[0].id;
|
||||
@@ -74,6 +145,7 @@ export default function add(pool) {
|
||||
const attachmentRes = await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.input('fileId', fileId)
|
||||
.input('sort', sort)
|
||||
.query(CREATE_ATTACHMENT);
|
||||
|
||||
const attachmentId = attachmentRes.recordset[0].id;
|
||||
@@ -84,8 +156,13 @@ export default function add(pool) {
|
||||
id: attachmentId,
|
||||
manifestId,
|
||||
fileId,
|
||||
sort,
|
||||
hash,
|
||||
mimetype,
|
||||
name,
|
||||
thumbavif,
|
||||
thumbx,
|
||||
thumby,
|
||||
};
|
||||
} catch (err) {
|
||||
try {
|
||||
|
||||
@@ -13,12 +13,12 @@ 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, mimetype: string, name: string, content: Buffer}>} The attachment record with binary content.
|
||||
* @returns {Promise<{id: number, manifestId: number, fileId: number, sort: number, hash: string, mimetype: string, name: string, content: Buffer, thumbavif: Buffer|null, thumbx: number|null, thumby: number|null}>} The attachment record with binary content.
|
||||
* @throws {Error} If the manifest or attachment is not found, or user is not authorized.
|
||||
*
|
||||
* @example
|
||||
* const attachment = await store.manifestattachment.getById(42, 1, 5);
|
||||
* // => { id, manifestId, fileId, mimetype, name, content }
|
||||
* // => { id, manifestId, fileId, sort, hash, mimetype, name, content, thumbavif, thumbx, thumby }
|
||||
*/
|
||||
return async (googleAuthId, manifestId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
@@ -42,9 +42,14 @@ export default function getById(pool) {
|
||||
id: row.id,
|
||||
manifestId: row.manifestId,
|
||||
fileId: row.fileId,
|
||||
sort: row.sort,
|
||||
hash: row.hash,
|
||||
mimetype: row.mimetype,
|
||||
name: row.name,
|
||||
content: row.content,
|
||||
thumbavif: row.thumbavif ?? null,
|
||||
thumbx: row.thumbx ?? null,
|
||||
thumby: row.thumby ?? null,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,14 @@ function shape(row) {
|
||||
id: row.id,
|
||||
manifestId: row.manifestId,
|
||||
fileId: row.fileId,
|
||||
sort: row.sort,
|
||||
hash: row.hash,
|
||||
mimetype: row.mimetype,
|
||||
name: row.name,
|
||||
size: row.size,
|
||||
thumbavif: row.thumbavif ?? null,
|
||||
thumbx: row.thumbx ?? null,
|
||||
thumby: row.thumby ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,12 +28,12 @@ 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, mimetype: string, name: string, size: number}>>} 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, 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
|
||||
* const attachments = await store.manifestattachment.listByManifestId(42, 1);
|
||||
* // => [{ id, manifestId, fileId, mimetype, name, size }]
|
||||
* // => [{ id, manifestId, fileId, sort, hash, mimetype, name, size, thumbavif, thumbx, thumby }]
|
||||
*/
|
||||
return async (googleAuthId, manifestId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
88
src/store/manifestattachment/order.js
Normal file
88
src/store/manifestattachment/order.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
LIST_IDS_BY_MANIFEST,
|
||||
SET_TEMP_SORTS,
|
||||
UPDATE_SORT,
|
||||
} from '../../sql/manifestattachment.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import resolveManifestAccess from './resolveManifestAccess.js';
|
||||
import request from '../request.js';
|
||||
|
||||
export default function order(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const manifestAccess = resolveManifestAccess(pool);
|
||||
|
||||
/**
|
||||
* Reorders the attachments of a manifest.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} manifestId - The manifest ID.
|
||||
* @param {object|Array<number>} input - The reorder input or array of IDs.
|
||||
* @param {Array<number>} [input.ids] - The attachment ids in their new order.
|
||||
* @returns {Promise<{ids: Array<number>}>} The applied order.
|
||||
* @throws {Error} If the order is invalid.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.manifestattachment.order(42, 1, { ids: [12, 10, 15] });
|
||||
* // => { ids: [12, 10, 15] }
|
||||
*/
|
||||
return async (googleAuthId, manifestId, input) => {
|
||||
const rawIds = Array.isArray(input) ? input : input?.ids;
|
||||
if (!Array.isArray(rawIds)) {
|
||||
throw new Error('Invalid attachment order');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
await manifestAccess(companyId, manifestId);
|
||||
|
||||
const current = await pool
|
||||
.request()
|
||||
.input('manifestId', manifestId)
|
||||
.query(LIST_IDS_BY_MANIFEST);
|
||||
|
||||
const existing = new Set(current.recordset.map((r) => r.id));
|
||||
|
||||
if (rawIds.length !== existing.size) {
|
||||
throw new Error('Invalid attachment order');
|
||||
}
|
||||
|
||||
const seen = new Set();
|
||||
for (const id of rawIds) {
|
||||
if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) {
|
||||
throw new Error('Invalid attachment order');
|
||||
}
|
||||
seen.add(id);
|
||||
}
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.query(SET_TEMP_SORTS);
|
||||
|
||||
for (let sort = 0; sort < rawIds.length; sort++) {
|
||||
const updateRes = await request(pool, tx)
|
||||
.input('id', rawIds[sort])
|
||||
.input('manifestId', manifestId)
|
||||
.input('sort', sort)
|
||||
.query(UPDATE_SORT);
|
||||
|
||||
if (!updateRes.rowsAffected[0]) {
|
||||
throw new Error('Invalid attachment order');
|
||||
}
|
||||
}
|
||||
|
||||
await tx.commit();
|
||||
|
||||
return { ids: rawIds };
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user