u
This commit is contained in:
24
README.md
24
README.md
@@ -146,7 +146,7 @@ Backend mapping:
|
|||||||
| `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` |
|
| `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` |
|
||||||
| `addImage` | `(googleAuthId, sku, imageBuffer)` | image result (see below) |
|
| `addImage` | `(googleAuthId, sku, imageBuffer)` | image result (see below) |
|
||||||
|
|
||||||
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.binary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Computes `hash` / `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Computes `hash` / `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
|
||||||
|
|
||||||
`addImage` returns:
|
`addImage` returns:
|
||||||
|
|
||||||
@@ -188,6 +188,26 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## `pic`
|
||||||
|
|
||||||
|
Lookup by `pic.hash` (SHA-256 hex from `addImage`), not `thumbhash`.
|
||||||
|
|
||||||
|
| Method | Signature | Returns |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `getImageByHash` | `(googleAuthId, hash)` | `{ buffer, w, h, size, contentType }` |
|
||||||
|
| `getThumbByHash` | `(googleAuthId, hash)` | `{ buffer, w, h, contentType }` |
|
||||||
|
|
||||||
|
Auth via `articlepic` → `categoryarticle` → `resolveCategoryAccess`. Errors: `Image not found`, `Not authorized`.
|
||||||
|
|
||||||
|
`contentType` is always `image/avif`. In dashback:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { buffer, contentType } = await store.pic.getImageByHash(req.user.id, hash);
|
||||||
|
res.set('Content-Type', contentType).send(buffer);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Backend route cheatsheet
|
## Backend route cheatsheet
|
||||||
|
|
||||||
| Route | Store call |
|
| Route | Store call |
|
||||||
@@ -204,6 +224,8 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err
|
|||||||
| `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` |
|
| `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` |
|
||||||
| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name })` |
|
| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name })` |
|
||||||
| `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` |
|
| `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` |
|
||||||
|
| `GET /api/image/:hash` | `pic.getImageByHash(googleAuthId, hash)` |
|
||||||
|
| `GET /api/image/:hash/thumb` | `pic.getThumbByHash(googleAuthId, hash)` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ const MENU = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
pic: {
|
||||||
|
label: 'pic',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
value: 'pic.getImageByHash',
|
||||||
|
label: 'getImageByHash',
|
||||||
|
hint: 'googleAuthId, hash → meta',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'pic.getThumbByHash',
|
||||||
|
label: 'getThumbByHash',
|
||||||
|
hint: 'googleAuthId, hash → meta',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
companyuser: {
|
companyuser: {
|
||||||
label: 'companyuser',
|
label: 'companyuser',
|
||||||
items: [
|
items: [
|
||||||
@@ -443,6 +458,43 @@ async function runMethod(client, action) {
|
|||||||
await client.article.addImage(Number(googleAuthId), sku, image),
|
await client.article.addImage(Number(googleAuthId), sku, image),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
case 'pic.getImageByHash': {
|
||||||
|
const googleAuthId = await askInt('google_auth id');
|
||||||
|
if (googleAuthId == null) return;
|
||||||
|
const hash = await askText('hash (SHA-256 hex)', {
|
||||||
|
validate: (v) => (!v?.trim() ? 'Required' : undefined),
|
||||||
|
});
|
||||||
|
if (hash == null) return;
|
||||||
|
const image = await client.pic.getImageByHash(Number(googleAuthId), hash.trim());
|
||||||
|
return [
|
||||||
|
'result',
|
||||||
|
{
|
||||||
|
w: image.w,
|
||||||
|
h: image.h,
|
||||||
|
size: image.size,
|
||||||
|
contentType: image.contentType,
|
||||||
|
bufferLength: image.buffer.length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case 'pic.getThumbByHash': {
|
||||||
|
const googleAuthId = await askInt('google_auth id');
|
||||||
|
if (googleAuthId == null) return;
|
||||||
|
const hash = await askText('hash (SHA-256 hex)', {
|
||||||
|
validate: (v) => (!v?.trim() ? 'Required' : undefined),
|
||||||
|
});
|
||||||
|
if (hash == null) return;
|
||||||
|
const image = await client.pic.getThumbByHash(Number(googleAuthId), hash.trim());
|
||||||
|
return [
|
||||||
|
'result',
|
||||||
|
{
|
||||||
|
w: image.w,
|
||||||
|
h: image.h,
|
||||||
|
contentType: image.contentType,
|
||||||
|
bufferLength: image.buffer.length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
case 'companyuser.add': {
|
case 'companyuser.add': {
|
||||||
const companyId = await askInt('Company id');
|
const companyId = await askInt('Company id');
|
||||||
if (companyId == null) return;
|
if (companyId == null) return;
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import googleAuthGetByEmail from './store/google_auth/getByEmail.js';
|
|||||||
import googleAuthListAllowedEmails from './store/google_auth/listAllowedEmails.js';
|
import googleAuthListAllowedEmails from './store/google_auth/listAllowedEmails.js';
|
||||||
import googleAuthRemoveAllowedEmail from './store/google_auth/removeAllowedEmail.js';
|
import googleAuthRemoveAllowedEmail from './store/google_auth/removeAllowedEmail.js';
|
||||||
import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js';
|
import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js';
|
||||||
|
import picGetImageByHash from './store/pic/getImageByHash.js';
|
||||||
|
import picGetThumbByHash from './store/pic/getThumbByHash.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';
|
||||||
|
|
||||||
@@ -84,6 +86,11 @@ export default class LibDashStore {
|
|||||||
setSelectedCompany: googleAuthSetSelectedCompany(pool),
|
setSelectedCompany: googleAuthSetSelectedCompany(pool),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.pic = {
|
||||||
|
getImageByHash: picGetImageByHash(pool),
|
||||||
|
getThumbByHash: picGetThumbByHash(pool),
|
||||||
|
};
|
||||||
|
|
||||||
this.purge = storePurge(pool);
|
this.purge = storePurge(pool);
|
||||||
this.initialize = storeInitialize(pool);
|
this.initialize = storeInitialize(pool);
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ BEGIN
|
|||||||
thumbh INT NOT NULL,
|
thumbh INT NOT NULL,
|
||||||
thumbhash NVARCHAR(64) NOT NULL,
|
thumbhash NVARCHAR(64) NOT NULL,
|
||||||
source NVARCHAR(MAX) NOT NULL,
|
source NVARCHAR(MAX) NOT NULL,
|
||||||
[binary] VARBINARY(MAX) NOT NULL,
|
fullbinary VARBINARY(MAX) NOT NULL,
|
||||||
thumbbinary VARBINARY(MAX) NOT NULL
|
thumbbinary VARBINARY(MAX) NOT NULL
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
export const CREATE = `
|
export const CREATE = `
|
||||||
INSERT INTO pic (
|
INSERT INTO pic (
|
||||||
w, h, size, hash, thumbw, thumbh, thumbhash, source, [binary], thumbbinary
|
w, h, size, hash, thumbw, thumbh, thumbhash, source, fullbinary, thumbbinary
|
||||||
)
|
)
|
||||||
OUTPUT INSERTED.id
|
OUTPUT INSERTED.id
|
||||||
VALUES (
|
VALUES (
|
||||||
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source, @binary, @thumbbinary
|
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @source, @fullbinary, @thumbbinary
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GET_CATEGORY_ID_BY_HASH = `
|
||||||
|
SELECT TOP 1 ca.category_id AS categoryId
|
||||||
|
FROM pic p
|
||||||
|
INNER JOIN articlepic ap ON ap.pic_id = p.id
|
||||||
|
INNER JOIN categoryarticle ca ON ca.article_id = ap.article_id
|
||||||
|
WHERE p.hash = @hash
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_FULL_BY_HASH = `
|
||||||
|
SELECT w, h, size, fullbinary AS buffer
|
||||||
|
FROM pic
|
||||||
|
WHERE hash = @hash
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_THUMB_BY_HASH = `
|
||||||
|
SELECT thumbw AS w, thumbh AS h, thumbbinary AS buffer
|
||||||
|
FROM pic
|
||||||
|
WHERE hash = @hash
|
||||||
|
`;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function addImage(pool) {
|
|||||||
.input('thumbh', processed.thumb.h)
|
.input('thumbh', processed.thumb.h)
|
||||||
.input('thumbhash', processed.thumb.hash)
|
.input('thumbhash', processed.thumb.hash)
|
||||||
.input('source', '')
|
.input('source', '')
|
||||||
.input('binary', processed.full.buffer)
|
.input('fullbinary', processed.full.buffer)
|
||||||
.input('thumbbinary', processed.thumb.buffer)
|
.input('thumbbinary', processed.thumb.buffer)
|
||||||
.query(CREATE_PIC);
|
.query(CREATE_PIC);
|
||||||
|
|
||||||
|
|||||||
28
src/store/pic/getImageByHash.js
Normal file
28
src/store/pic/getImageByHash.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { GET_FULL_BY_HASH } from '../../sql/pic.js';
|
||||||
|
import resolvePicAccess from './resolvePicAccess.js';
|
||||||
|
|
||||||
|
export default function getImageByHash(pool) {
|
||||||
|
const authorize = resolvePicAccess(pool);
|
||||||
|
|
||||||
|
return async (googleAuthId, hash) => {
|
||||||
|
await authorize(googleAuthId, hash);
|
||||||
|
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('hash', hash)
|
||||||
|
.query(GET_FULL_BY_HASH);
|
||||||
|
|
||||||
|
const row = result.recordset[0];
|
||||||
|
if (!row?.buffer) {
|
||||||
|
throw new Error('Image not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: row.buffer,
|
||||||
|
w: row.w,
|
||||||
|
h: row.h,
|
||||||
|
size: row.size,
|
||||||
|
contentType: 'image/avif',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
27
src/store/pic/getThumbByHash.js
Normal file
27
src/store/pic/getThumbByHash.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { GET_THUMB_BY_HASH } from '../../sql/pic.js';
|
||||||
|
import resolvePicAccess from './resolvePicAccess.js';
|
||||||
|
|
||||||
|
export default function getThumbByHash(pool) {
|
||||||
|
const authorize = resolvePicAccess(pool);
|
||||||
|
|
||||||
|
return async (googleAuthId, hash) => {
|
||||||
|
await authorize(googleAuthId, hash);
|
||||||
|
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('hash', hash)
|
||||||
|
.query(GET_THUMB_BY_HASH);
|
||||||
|
|
||||||
|
const row = result.recordset[0];
|
||||||
|
if (!row?.buffer) {
|
||||||
|
throw new Error('Image not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: row.buffer,
|
||||||
|
w: row.w,
|
||||||
|
h: row.h,
|
||||||
|
contentType: 'image/avif',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
20
src/store/pic/resolvePicAccess.js
Normal file
20
src/store/pic/resolvePicAccess.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { GET_CATEGORY_ID_BY_HASH } from '../../sql/pic.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
|
||||||
|
export default function resolvePicAccess(pool) {
|
||||||
|
const access = resolveCategoryAccess(pool);
|
||||||
|
|
||||||
|
return async (googleAuthId, hash) => {
|
||||||
|
const result = await pool
|
||||||
|
.request()
|
||||||
|
.input('hash', hash)
|
||||||
|
.query(GET_CATEGORY_ID_BY_HASH);
|
||||||
|
|
||||||
|
const categoryId = result.recordset[0]?.categoryId;
|
||||||
|
if (!categoryId) {
|
||||||
|
throw new Error('Image not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await access(googleAuthId, categoryId);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user