u
This commit is contained in:
@@ -134,9 +134,9 @@
|
||||
* type: number
|
||||
* nullable: true
|
||||
* unit:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/Unit'
|
||||
* brand:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/ArticleBrand'
|
||||
* nullable: true
|
||||
* categoryIds:
|
||||
* type: array
|
||||
@@ -353,7 +353,157 @@
|
||||
* type: array
|
||||
* description: Brand pictures sorted by sort
|
||||
* items:
|
||||
* $ref: '#/components/schemas/BrandPic'
|
||||
* ArticleBrand:
|
||||
* type: object
|
||||
* required: [name]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* name:
|
||||
* type: string
|
||||
* fullHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* thumbW:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbH:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* StuecklisteItem:
|
||||
* type: object
|
||||
* required: [id, articleId, sku, name, count, sort]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* articleId:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* sort:
|
||||
* type: integer
|
||||
* ArticleChild:
|
||||
* type: object
|
||||
* required: [id, sku, name, parentsort, attributeValueId, attributeValue]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* parentsort:
|
||||
* type: integer
|
||||
* attributeValueId:
|
||||
* type: integer
|
||||
* attributeValue:
|
||||
* type: string
|
||||
* ArticleOptionItem:
|
||||
* type: object
|
||||
* required: [id, articleId, sku, name, count, sort, mandated]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* articleId:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* sort:
|
||||
* type: integer
|
||||
* mandated:
|
||||
* type: boolean
|
||||
* CategoryTreeNode:
|
||||
* type: object
|
||||
* required: [id, sort, posterminalIds]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* parentId:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* sort:
|
||||
* type: integer
|
||||
* name:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* slug:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* fullHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* thumbW:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbH:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* posterminalIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* CategoryArticle:
|
||||
* type: object
|
||||
* required: [id, sku, name, slug, track_stock, optionsCount, articleListCount, childCount]
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* slug:
|
||||
* type: string
|
||||
* net:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* taxRate:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* track_stock:
|
||||
* type: boolean
|
||||
* optionsCount:
|
||||
* type: integer
|
||||
* articleListCount:
|
||||
* type: integer
|
||||
* childCount:
|
||||
* type: integer
|
||||
* parentArticleId:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* parentSku:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* fullHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* thumbW:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbH:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* brand:
|
||||
* $ref: '#/components/schemas/ArticleBrand'
|
||||
* nullable: true
|
||||
* NumberSequence:
|
||||
* type: object
|
||||
* required: [id, type, prefix, padding, next]
|
||||
@@ -1151,6 +1301,20 @@
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* required: [manifestarticle_id, sku, name, warehouse_id, amount, text]
|
||||
* properties:
|
||||
* manifestarticle_id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* warehouse_id:
|
||||
* type: integer
|
||||
* amount:
|
||||
* type: number
|
||||
* text:
|
||||
* type: string
|
||||
* ledger:
|
||||
* type: array
|
||||
* items:
|
||||
|
||||
@@ -11,7 +11,7 @@ import { emit } from '../sse.js';
|
||||
import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js';
|
||||
|
||||
const AVIF_BODY_LIMIT = '20mb';
|
||||
const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/;
|
||||
const FULL_HASH_RE = /^[a-zA-Z0-9]{8}$/;
|
||||
|
||||
/** Notify SSE subscribers after create / rename-ish list-level article changes. */
|
||||
function emitArticlesSignal(req) {
|
||||
@@ -44,9 +44,9 @@ function parseSku(value) {
|
||||
function parseFullHash(value) {
|
||||
const fullHash = typeof value === 'string' ? value.trim() : '';
|
||||
if (!FULL_HASH_RE.test(fullHash)) {
|
||||
return { error: 'fullHash must be a 64-char hex SHA-256' };
|
||||
return { error: 'fullHash must be an 8-char alphanumeric string' };
|
||||
}
|
||||
return { fullHash: fullHash.toLowerCase() };
|
||||
return { fullHash };
|
||||
}
|
||||
|
||||
function articleErrorStatus(error) {
|
||||
@@ -720,7 +720,8 @@ export function register(app, basePath = '') {
|
||||
* summary: Replace POS prices by SKU
|
||||
* description: |
|
||||
* Replace-all prices for the company POS customer group.
|
||||
* Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).
|
||||
* Each entry accepts `net` OR `gross` price (or legacy `price`), `taxRate` OR `tax` (percentage, e.g. 19 for 19%, ensured via tax.ensure), optional `minmultiplier` (default 1).
|
||||
* When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
|
||||
* Empty `prices` clears all POS prices. Returns the same shape as GET by SKU (article `prices` array contains `{ minmultiplier, net, taxRate, customergroupId }`).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
@@ -744,23 +745,31 @@ export function register(app, basePath = '') {
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* required: [price, tax]
|
||||
* properties:
|
||||
* net:
|
||||
* type: number
|
||||
* description: Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required.
|
||||
* gross:
|
||||
* type: number
|
||||
* description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`.
|
||||
* price:
|
||||
* type: number
|
||||
* description: Net price
|
||||
* description: Legacy alias for `net`.
|
||||
* taxRate:
|
||||
* type: number
|
||||
* description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%).
|
||||
* tax:
|
||||
* type: number
|
||||
* description: Tax rate (e.g. 19)
|
||||
* description: Alias for `taxRate` percentage.
|
||||
* minmultiplier:
|
||||
* type: number
|
||||
* description: Defaults to 1; must be unique per request
|
||||
* description: Defaults to 1; must be unique per request.
|
||||
* example:
|
||||
* prices:
|
||||
* - price: 10.5
|
||||
* tax: 19
|
||||
* - price: 9.9
|
||||
* tax: 19
|
||||
* - net: 10.5
|
||||
* taxRate: 19
|
||||
* - gross: 11.9
|
||||
* taxRate: 19
|
||||
* minmultiplier: 10
|
||||
* responses:
|
||||
* 200:
|
||||
@@ -936,7 +945,7 @@ export function register(app, basePath = '') {
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* pattern: '^[a-fA-F0-9]{64}$'
|
||||
* pattern: '^[a-zA-Z0-9]{8}$'
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Deleted
|
||||
@@ -1017,11 +1026,11 @@ export function register(app, basePath = '') {
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* pattern: '^[a-fA-F0-9]{64}$'
|
||||
* pattern: '^[a-zA-Z0-9]{8}$'
|
||||
* example:
|
||||
* fullHashes:
|
||||
* - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
* - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
* - "aB1cD2eF"
|
||||
* - "gH3iJ4kL"
|
||||
* responses:
|
||||
* 200:
|
||||
* description: New order
|
||||
@@ -1806,20 +1815,7 @@ export function register(app, basePath = '') {
|
||||
* children:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* parentsort:
|
||||
* type: integer
|
||||
* attributeValueId:
|
||||
* type: integer
|
||||
* attributeValue:
|
||||
* type: string
|
||||
* $ref: '#/components/schemas/ArticleChild'
|
||||
* 404:
|
||||
* description: Article not found
|
||||
*/
|
||||
@@ -1930,7 +1926,7 @@ export function register(app, basePath = '') {
|
||||
* children:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/ArticleChild'
|
||||
* 400:
|
||||
* description: Missing sku or attribute value, or invalid input
|
||||
* 404:
|
||||
@@ -2080,20 +2076,7 @@ export function register(app, basePath = '') {
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* articleId:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* sort:
|
||||
* type: integer
|
||||
* $ref: '#/components/schemas/StuecklisteItem'
|
||||
* 404:
|
||||
* description: Article not found
|
||||
*/
|
||||
@@ -2161,7 +2144,7 @@ export function register(app, basePath = '') {
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/StuecklisteItem'
|
||||
* 400:
|
||||
* description: Article is a parent, has options, or contains itself
|
||||
* 404:
|
||||
@@ -2216,22 +2199,7 @@ export function register(app, basePath = '') {
|
||||
* options:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* articleId:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* count:
|
||||
* type: integer
|
||||
* sort:
|
||||
* type: integer
|
||||
* mandated:
|
||||
* type: boolean
|
||||
* $ref: '#/components/schemas/ArticleOptionItem'
|
||||
* 404:
|
||||
* description: Article not found
|
||||
*/
|
||||
@@ -2302,7 +2270,7 @@ export function register(app, basePath = '') {
|
||||
* options:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* $ref: '#/components/schemas/ArticleOptionItem'
|
||||
* 400:
|
||||
* description: Article is a parent, a Stückliste, or contains itself
|
||||
* 404:
|
||||
|
||||
@@ -74,13 +74,11 @@ export function register(app, basePath = '') {
|
||||
* tree:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* additionalProperties: true
|
||||
* $ref: '#/components/schemas/CategoryTreeNode'
|
||||
* articles:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* additionalProperties: true
|
||||
* $ref: '#/components/schemas/CategoryArticle'
|
||||
* 400:
|
||||
* description: No company selected
|
||||
* content:
|
||||
@@ -326,48 +324,7 @@ export function register(app, basePath = '') {
|
||||
* articles:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* sku:
|
||||
* type: string
|
||||
* name:
|
||||
* type: string
|
||||
* slug:
|
||||
* type: string
|
||||
* net:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* taxRate:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* track_stock:
|
||||
* type: boolean
|
||||
* optionsCount:
|
||||
* type: integer
|
||||
* articleListCount:
|
||||
* type: integer
|
||||
* childCount:
|
||||
* type: integer
|
||||
* description: Number of child articles belonging to this article
|
||||
* fullHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* thumbW:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbH:
|
||||
* type: integer
|
||||
* nullable: true
|
||||
* thumbHash:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* brand:
|
||||
* type: object
|
||||
* nullable: true
|
||||
* additionalProperties: true
|
||||
* additionalProperties: true
|
||||
* $ref: '#/components/schemas/CategoryArticle'
|
||||
* 400:
|
||||
* description: Invalid id
|
||||
* content:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as db from '../../db/pic.js';
|
||||
|
||||
const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/;
|
||||
/** Content-addressed by SHA-256 — safe for long-lived browser cache (auth still required on first fetch). */
|
||||
const FULL_HASH_RE = /^[a-zA-Z0-9]{8}$/;
|
||||
/** Content-addressed by SHA-256 (Base62, 8 chars) — safe for long-lived browser cache (auth still required on first fetch). */
|
||||
const CACHE_CONTROL = 'private, max-age=31536000, immutable';
|
||||
|
||||
function parseFullHash(value) {
|
||||
const fullHash = typeof value === 'string' ? value.trim() : '';
|
||||
if (!FULL_HASH_RE.test(fullHash)) {
|
||||
return { error: 'fullHash must be a 64-char hex SHA-256' };
|
||||
return { error: 'fullHash must be an 8-char alphanumeric string' };
|
||||
}
|
||||
return { fullHash: fullHash.toLowerCase() };
|
||||
return { fullHash };
|
||||
}
|
||||
|
||||
function picErrorStatus(error) {
|
||||
@@ -47,7 +47,7 @@ export function register(app, basePath = '') {
|
||||
* tags: [Image]
|
||||
* summary: Full image by hash
|
||||
* description: |
|
||||
* Returns the full AVIF by `pic.hash` (SHA-256 hex).
|
||||
* Returns the full AVIF by `pic.hash` (8-char alphanumeric Base62).
|
||||
* Auth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
@@ -58,8 +58,8 @@ export function register(app, basePath = '') {
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* pattern: '^[a-fA-F0-9]{64}$'
|
||||
* description: SHA-256 hex of the full image bytes
|
||||
* pattern: '^[a-zA-Z0-9]{8}$'
|
||||
* description: 8-char alphanumeric hash of the full image bytes
|
||||
* responses:
|
||||
* 200:
|
||||
* description: AVIF image bytes
|
||||
@@ -143,8 +143,8 @@ export function register(app, basePath = '') {
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* pattern: '^[a-fA-F0-9]{64}$'
|
||||
* description: SHA-256 hex of the full image bytes
|
||||
* pattern: '^[a-zA-Z0-9]{8}$'
|
||||
* description: 8-char alphanumeric hash of the full image bytes
|
||||
* responses:
|
||||
* 200:
|
||||
* description: AVIF thumb bytes
|
||||
|
||||
@@ -479,7 +479,7 @@ export function register(app, basePath = '') {
|
||||
* Files are written to the company's VFS root directory and saved to the database.
|
||||
* An optional `targetDir` form field specifies a subfolder within the VFS.
|
||||
*
|
||||
* Writing to read-only database paths (`image/*` or filenames containing 64-character hex hashes) is
|
||||
* Writing to read-only database paths (`image/*` or filenames containing 8-character image hashes) is
|
||||
* forbidden and automatically skipped during upload.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
@@ -813,7 +813,7 @@ export function register(app, basePath = '') {
|
||||
* - Binary body: raw binary buffer with optional `Content-Type` header
|
||||
* - Plain text: raw UTF-8 string
|
||||
*
|
||||
* Modifying read-only database paths (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403.
|
||||
* Modifying read-only database paths (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-signals: [vfs]
|
||||
@@ -959,7 +959,7 @@ export function register(app, basePath = '') {
|
||||
* summary: Delete a virtual filesystem file
|
||||
* description: |
|
||||
* Deletes a file from both the on-disk virtual filesystem and database for the authenticated user's selected company.
|
||||
* Deleting read-only database files (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403.
|
||||
* Deleting read-only database files (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403.
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-signals: [vfs]
|
||||
|
||||
@@ -236,7 +236,7 @@ function extractImageInputs(args = {}) {
|
||||
|
||||
/**
|
||||
* Fetches a database image by its content hash using store or authenticated local endpoint.
|
||||
* @param {string} hash 64-char SHA-256 hex
|
||||
* @param {string} hash 8-char SHA-256 Base62
|
||||
* @param {number} googleAuthId
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
@@ -323,7 +323,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
|
||||
// ── Load the bytes ────────────────────────────────────────────
|
||||
const items = [];
|
||||
for (const item of inputs) {
|
||||
// Only a path that actually carries a 64-char content hash is a database image.
|
||||
// Only a path that actually carries an 8-char content hash is a database image.
|
||||
// A bare "images/…" prefix is just the VFS folder, so it must NOT be treated as
|
||||
// a DB lookup or `images/candidates/x.avif` would be misread as a hash path.
|
||||
const hash = extractFullHash(item.path);
|
||||
@@ -355,7 +355,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
|
||||
throw new ToolError(
|
||||
`Image not found or empty: "${item.path}". ` +
|
||||
(hash
|
||||
? 'Database images must be addressed as "image/<fullHash>" with a 64-char hash.'
|
||||
? 'Database images must be addressed as "image/<fullHash>" with an 8-char hash.'
|
||||
: 'Store it first (search_images writes to "images/…") or check the path with find_files.')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ function resolveImagesApiUrl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a database image by its 64-char content hash using store or authenticated local endpoint.
|
||||
* Fetches a database image by its 8-char content hash using store or authenticated local endpoint.
|
||||
* @param {string} hash
|
||||
* @param {number} googleAuthId
|
||||
* @returns {Promise<Buffer>}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getStore } from '../db/store.js';
|
||||
import env from '../config/env.js';
|
||||
import { getDefaultSkills } from './skill.js';
|
||||
|
||||
const FULL_HASH_REGEX = /^[a-fA-F0-9]{64}$/;
|
||||
const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/;
|
||||
const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']);
|
||||
|
||||
export function getMimeType(filePath) {
|
||||
@@ -55,18 +55,26 @@ export function getMimeType(filePath) {
|
||||
export function extractFullHash(p) {
|
||||
if (typeof p !== 'string' || !p.trim()) return null;
|
||||
const clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '');
|
||||
const parts = clean.split('/');
|
||||
const parts = clean.split('/').filter(Boolean);
|
||||
if (parts.length === 0) return null;
|
||||
|
||||
const first = parts[0].toLowerCase();
|
||||
const last = parts[parts.length - 1];
|
||||
const ext = path.extname(last).toLowerCase();
|
||||
const withoutExt = last.replace(/\.[a-zA-Z0-9]+$/, '');
|
||||
if (FULL_HASH_REGEX.test(withoutExt)) {
|
||||
return withoutExt.toLowerCase();
|
||||
}
|
||||
for (const part of parts) {
|
||||
const seg = part.replace(/\.[a-zA-Z0-9]+$/, '');
|
||||
if (FULL_HASH_REGEX.test(seg)) {
|
||||
return seg.toLowerCase();
|
||||
|
||||
// Under virtual image folder (e.g. image/aB1cD2eF, images/aB1cD2eF.avif)
|
||||
if (VIRTUAL_IMAGE_FOLDERS.has(first)) {
|
||||
if (parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) {
|
||||
return withoutExt;
|
||||
}
|
||||
}
|
||||
|
||||
// Direct hash (e.g. "aB1cD2eF" or "aB1cD2eF.avif")
|
||||
if (parts.length === 1 && FULL_HASH_REGEX.test(withoutExt) && (!ext || ext === '.avif')) {
|
||||
return withoutExt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user