@open-mercato/core 0.9.1-develop.7459.1.99e3accd30 → 0.9.1-develop.7462.1.94c09f158f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/api/route.js +29 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/ocrLimits.js +49 -0
- package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
- package/dist/modules/attachments/lib/ocrQueue.js +76 -2
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +4 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
- package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/textExtraction.js +13 -1
- package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
- package/dist/modules/catalog/search.js +3 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
- package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/attachments/api/route.ts +36 -3
- package/src/modules/attachments/lib/ocrLimits.ts +62 -0
- package/src/modules/attachments/lib/ocrQueue.ts +95 -2
- package/src/modules/attachments/lib/ocrService.ts +4 -0
- package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
- package/src/modules/attachments/lib/textExtraction.ts +14 -1
- package/src/modules/catalog/search.ts +5 -1
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
- package/src/modules/communication_channels/i18n/de.json +0 -1
- package/src/modules/communication_channels/i18n/en.json +0 -1
- package/src/modules/communication_channels/i18n/es.json +0 -1
- package/src/modules/communication_channels/i18n/ko.json +0 -1
- package/src/modules/communication_channels/i18n/pl.json +0 -1
- package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/ocrService.ts"],
|
|
4
|
-
"sourcesContent": ["import { generateText } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'ocr-service' })\n\nexport type OcrServiceOptions = {\n apiKey?: string\n model?: string\n}\n\nexport type OcrInput = {\n filePath: string\n mimeType: string | null\n model?: string\n}\n\nexport type OcrResult = {\n content: string\n pageCount?: number\n processingTimeMs: number\n}\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst OCR_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff']\n\nconst DEFAULT_OCR_PROMPT = `Extract all text content from this image. Preserve the structure and formatting where possible. Output the text in markdown format. If there are tables, preserve them as markdown tables. If there is no text visible, respond with an empty string.`\n\nfunction isImageMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return OCR_IMAGE_EXTENSIONS.includes(ext)\n }\n return false\n}\n\nfunction isPdfMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized === 'application/pdf') return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ext === '.pdf'\n }\n return false\n}\n\nfunction getImageMediaType(mimeType: string | null, filePath: string): string {\n if (mimeType && mimeType.startsWith('image/')) {\n return mimeType\n }\n const ext = path.extname(filePath).toLowerCase()\n const mimeMap: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.bmp': 'image/bmp',\n '.webp': 'image/webp',\n '.tiff': 'image/tiff',\n }\n return mimeMap[ext] || 'image/png'\n}\n\nexport class OcrService {\n private readonly apiKey: string | null\n private readonly defaultModel: string\n private client: ReturnType<typeof createOpenAI> | null = null\n\n constructor(opts: OcrServiceOptions = {}) {\n this.apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY ?? null\n this.defaultModel = opts.model ?? process.env.OCR_MODEL ?? DEFAULT_MODEL\n }\n\n get available(): boolean {\n return Boolean(this.apiKey)\n }\n\n private ensureClient() {\n if (!this.apiKey) {\n throw new Error('[attachments.ocr] Missing OPENAI_API_KEY environment variable')\n }\n if (!this.client) {\n this.client = createOpenAI({ apiKey: this.apiKey })\n }\n return this.client\n }\n\n private async generateTextFromImageBuffer(params: {\n imageBuffer: Buffer\n mediaType: string\n model: string\n }): Promise<string> {\n const { imageBuffer, mediaType, model } = params\n const client = this.ensureClient()\n const base64 = imageBuffer.toString('base64')\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n try {\n const result = await generateText({\n model: client(model),\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n image: dataUrl,\n },\n {\n type: 'text',\n text: process.env.OCR_DEFAULT_PROMPT ?? DEFAULT_OCR_PROMPT,\n },\n ],\n },\n ],\n })\n\n return result.text.trim()\n } catch (err: any) {\n const statusCandidate =\n err?.statusCode ?? err?.status ?? err?.response?.status ?? err?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = err?.data?.error ?? err?.body?.error ?? err?.response?.data?.error\n const apiMessage = apiError?.message ?? err?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof err?.message === 'string' ? err.message : 'OCR request failed')\n\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = 'OpenAI usage quota exceeded. Please review your plan and billing.'\n break\n case 'invalid_api_key':\n guidance = 'Invalid OpenAI API key. Update the key and retry.'\n break\n case 'account_deactivated':\n guidance = 'OpenAI account is disabled. Contact OpenAI support or provide a different key.'\n break\n case 'rate_limit_exceeded':\n guidance = 'Rate limit exceeded. Please try again later.'\n break\n default:\n guidance = rawMessage.includes('https://')\n ? rawMessage\n : `${rawMessage}. Check OPENAI_API_KEY.`\n }\n\n const wrapped = new Error(`[attachments.ocr] ${guidance}`)\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n (wrapped as any).status = normalizedStatus\n }\n }\n if (apiCode) {\n (wrapped as any).code = apiCode\n }\n (wrapped as any).cause = err\n throw wrapped\n }\n }\n\n async processImage(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, mimeType, model } = input\n const resolvedModel = model ?? this.defaultModel\n const imageBuffer = await fs.readFile(filePath)\n\n return {\n content: await this.generateTextFromImageBuffer({\n imageBuffer,\n mediaType: getImageMediaType(mimeType, filePath),\n model: resolvedModel,\n }),\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processPdf(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, model } = input\n const resolvedModel = model ?? this.defaultModel\n const { preparePdfPagesForOcr } = await import('./pdfProcessing')\n const preparedPdf = await preparePdfPagesForOcr(filePath)\n\n if (preparedPdf.pageCount === 0) {\n return {\n content: '',\n pageCount: 0,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n const pageContents: string[] = []\n\n for (const page of preparedPdf.pages) {\n try {\n let pageContent = page.extractedText\n\n if (!pageContent && page.imageBuffer) {\n pageContent = await this.generateTextFromImageBuffer({\n imageBuffer: page.imageBuffer,\n mediaType: 'image/png',\n model: resolvedModel,\n })\n }\n\n if (pageContent) {\n if (preparedPdf.pageCount > 1) {\n pageContents.push(`--- Page ${page.pageNumber} ---\\n\\n${pageContent}`)\n } else {\n pageContents.push(pageContent)\n }\n }\n } catch (error) {\n logger.error('Failed to process PDF page', { pageNumber: page.pageNumber, err: error })\n }\n }\n\n return {\n content: pageContents.join('\\n\\n'),\n pageCount: preparedPdf.pageCount,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processFile(input: OcrInput): Promise<OcrResult | null> {\n const { filePath, mimeType } = input\n\n if (isPdfMimeType(mimeType, filePath)) {\n logger.info('Processing PDF attachment via pdfjs-dist')\n return this.processPdf(input)\n }\n\n if (isImageMimeType(mimeType, filePath)) {\n logger.info('Processing image attachment via LLM OCR')\n return this.processImage(input)\n }\n\n logger.info('Unsupported file type for OCR', { mimeType: mimeType || 'unknown' })\n return null\n }\n}\n\nexport function shouldUseLlmOcr(mimeType: string | null, fileName: string): boolean {\n if (isPdfMimeType(mimeType, fileName)) return true\n return isImageMimeType(mimeType, fileName)\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { generateText } from 'ai'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport fs from 'fs/promises'\nimport path from 'path'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveOcrMaxOutputTokens, resolveOcrPageTimeoutMs } from './ocrLimits'\n\nconst logger = createLogger('attachments').child({ component: 'ocr-service' })\n\nexport type OcrServiceOptions = {\n apiKey?: string\n model?: string\n}\n\nexport type OcrInput = {\n filePath: string\n mimeType: string | null\n model?: string\n}\n\nexport type OcrResult = {\n content: string\n pageCount?: number\n processingTimeMs: number\n}\n\nconst DEFAULT_MODEL = 'gpt-4o'\nconst OCR_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff']\n\nconst DEFAULT_OCR_PROMPT = `Extract all text content from this image. Preserve the structure and formatting where possible. Output the text in markdown format. If there are tables, preserve them as markdown tables. If there is no text visible, respond with an empty string.`\n\nfunction isImageMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return OCR_IMAGE_EXTENSIONS.includes(ext)\n }\n return false\n}\n\nfunction isPdfMimeType(mimeType: string | null, filePath?: string): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized === 'application/pdf') return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ext === '.pdf'\n }\n return false\n}\n\nfunction getImageMediaType(mimeType: string | null, filePath: string): string {\n if (mimeType && mimeType.startsWith('image/')) {\n return mimeType\n }\n const ext = path.extname(filePath).toLowerCase()\n const mimeMap: Record<string, string> = {\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.bmp': 'image/bmp',\n '.webp': 'image/webp',\n '.tiff': 'image/tiff',\n }\n return mimeMap[ext] || 'image/png'\n}\n\nexport class OcrService {\n private readonly apiKey: string | null\n private readonly defaultModel: string\n private client: ReturnType<typeof createOpenAI> | null = null\n\n constructor(opts: OcrServiceOptions = {}) {\n this.apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY ?? null\n this.defaultModel = opts.model ?? process.env.OCR_MODEL ?? DEFAULT_MODEL\n }\n\n get available(): boolean {\n return Boolean(this.apiKey)\n }\n\n private ensureClient() {\n if (!this.apiKey) {\n throw new Error('[attachments.ocr] Missing OPENAI_API_KEY environment variable')\n }\n if (!this.client) {\n this.client = createOpenAI({ apiKey: this.apiKey })\n }\n return this.client\n }\n\n private async generateTextFromImageBuffer(params: {\n imageBuffer: Buffer\n mediaType: string\n model: string\n }): Promise<string> {\n const { imageBuffer, mediaType, model } = params\n const client = this.ensureClient()\n const base64 = imageBuffer.toString('base64')\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n try {\n const result = await generateText({\n model: client(model),\n abortSignal: AbortSignal.timeout(resolveOcrPageTimeoutMs()),\n maxRetries: 0,\n maxOutputTokens: resolveOcrMaxOutputTokens(),\n messages: [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n image: dataUrl,\n },\n {\n type: 'text',\n text: process.env.OCR_DEFAULT_PROMPT ?? DEFAULT_OCR_PROMPT,\n },\n ],\n },\n ],\n })\n\n return result.text.trim()\n } catch (err: any) {\n const statusCandidate =\n err?.statusCode ?? err?.status ?? err?.response?.status ?? err?.response?.statusCode\n const status =\n typeof statusCandidate === 'number'\n ? Number.isFinite(statusCandidate) ? statusCandidate : undefined\n : typeof statusCandidate === 'string'\n ? Number.parseInt(statusCandidate, 10)\n : undefined\n const apiError = err?.data?.error ?? err?.body?.error ?? err?.response?.data?.error\n const apiMessage = apiError?.message ?? err?.response?.data?.message\n const apiCode = typeof apiError?.code === 'string' ? apiError.code : undefined\n const rawMessage = typeof apiMessage === 'string'\n ? apiMessage\n : (typeof err?.message === 'string' ? err.message : 'OCR request failed')\n\n let guidance: string\n switch (apiCode) {\n case 'insufficient_quota':\n guidance = 'OpenAI usage quota exceeded. Please review your plan and billing.'\n break\n case 'invalid_api_key':\n guidance = 'Invalid OpenAI API key. Update the key and retry.'\n break\n case 'account_deactivated':\n guidance = 'OpenAI account is disabled. Contact OpenAI support or provide a different key.'\n break\n case 'rate_limit_exceeded':\n guidance = 'Rate limit exceeded. Please try again later.'\n break\n default:\n guidance = rawMessage.includes('https://')\n ? rawMessage\n : `${rawMessage}. Check OPENAI_API_KEY.`\n }\n\n const wrapped = new Error(`[attachments.ocr] ${guidance}`)\n if (typeof status === 'number' && Number.isFinite(status)) {\n const normalizedStatus = status === 401 || status === 403 ? 502 : status\n if (normalizedStatus >= 400 && normalizedStatus < 600) {\n (wrapped as any).status = normalizedStatus\n }\n }\n if (apiCode) {\n (wrapped as any).code = apiCode\n }\n (wrapped as any).cause = err\n throw wrapped\n }\n }\n\n async processImage(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, mimeType, model } = input\n const resolvedModel = model ?? this.defaultModel\n const imageBuffer = await fs.readFile(filePath)\n\n return {\n content: await this.generateTextFromImageBuffer({\n imageBuffer,\n mediaType: getImageMediaType(mimeType, filePath),\n model: resolvedModel,\n }),\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processPdf(input: OcrInput): Promise<OcrResult> {\n const startTime = Date.now()\n const { filePath, model } = input\n const resolvedModel = model ?? this.defaultModel\n const { preparePdfPagesForOcr } = await import('./pdfProcessing')\n const preparedPdf = await preparePdfPagesForOcr(filePath)\n\n if (preparedPdf.pageCount === 0) {\n return {\n content: '',\n pageCount: 0,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n const pageContents: string[] = []\n\n for (const page of preparedPdf.pages) {\n try {\n let pageContent = page.extractedText\n\n if (!pageContent && page.imageBuffer) {\n pageContent = await this.generateTextFromImageBuffer({\n imageBuffer: page.imageBuffer,\n mediaType: 'image/png',\n model: resolvedModel,\n })\n }\n\n if (pageContent) {\n if (preparedPdf.pageCount > 1) {\n pageContents.push(`--- Page ${page.pageNumber} ---\\n\\n${pageContent}`)\n } else {\n pageContents.push(pageContent)\n }\n }\n } catch (error) {\n logger.error('Failed to process PDF page', { pageNumber: page.pageNumber, err: error })\n }\n }\n\n return {\n content: pageContents.join('\\n\\n'),\n pageCount: preparedPdf.pageCount,\n processingTimeMs: Date.now() - startTime,\n }\n }\n\n async processFile(input: OcrInput): Promise<OcrResult | null> {\n const { filePath, mimeType } = input\n\n if (isPdfMimeType(mimeType, filePath)) {\n logger.info('Processing PDF attachment via pdfjs-dist')\n return this.processPdf(input)\n }\n\n if (isImageMimeType(mimeType, filePath)) {\n logger.info('Processing image attachment via LLM OCR')\n return this.processImage(input)\n }\n\n logger.info('Unsupported file type for OCR', { mimeType: mimeType || 'unknown' })\n return null\n }\n}\n\nexport function shouldUseLlmOcr(mimeType: string | null, fileName: string): boolean {\n if (isPdfMimeType(mimeType, fileName)) return true\n return isImageMimeType(mimeType, fileName)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { resolveMaxOcrPages, resolvePdfPageIterationLimit } from "./ocrLimits.js";
|
|
4
6
|
const moduleRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
5
7
|
const pdfJsPackageRoot = path.dirname(moduleRequire.resolve("pdfjs-dist/package.json"));
|
|
6
8
|
const cMapUrl = `${path.join(pdfJsPackageRoot, "cmaps")}${path.sep}`;
|
|
7
9
|
const standardFontDataUrl = `${path.join(pdfJsPackageRoot, "standard_fonts")}${path.sep}`;
|
|
10
|
+
const logger = createLogger("attachments").child({ component: "pdf-processing" });
|
|
8
11
|
const MIN_RENDER_SCALE = 2;
|
|
9
12
|
const MAX_RENDER_SCALE = 4;
|
|
10
13
|
const MAX_RENDER_DIMENSION = 2200;
|
|
@@ -60,7 +63,16 @@ async function preparePdfPagesForOcr(filePath) {
|
|
|
60
63
|
const pdfDocument = await loadingTask.promise;
|
|
61
64
|
try {
|
|
62
65
|
const pages = [];
|
|
63
|
-
|
|
66
|
+
const maxPages = resolveMaxOcrPages();
|
|
67
|
+
const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages);
|
|
68
|
+
if (pdfDocument.numPages > pageLimit) {
|
|
69
|
+
logger.warn("PDF page count exceeds OCR cap; truncating", {
|
|
70
|
+
numPages: pdfDocument.numPages,
|
|
71
|
+
maxPages: pageLimit,
|
|
72
|
+
filePath
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
|
|
64
76
|
const page = await pdfDocument.getPage(pageNumber);
|
|
65
77
|
try {
|
|
66
78
|
const textContent = await page.getTextContent();
|
|
@@ -83,7 +95,7 @@ async function preparePdfPagesForOcr(filePath) {
|
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
return {
|
|
86
|
-
pageCount:
|
|
98
|
+
pageCount: pages.length,
|
|
87
99
|
pages
|
|
88
100
|
};
|
|
89
101
|
} finally {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/pdfProcessing.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst cMapUrl = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst standardFontDataUrl = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\nconst MIN_RENDER_SCALE = 2\nconst MAX_RENDER_SCALE = 4\nconst MAX_RENDER_DIMENSION = 2200\nconst MAX_RENDER_PIXELS = 8_000_000\n\ntype PdfPageProxyLike = {\n cleanup: () => void\n getTextContent: () => Promise<{ items?: Array<{ str?: string }> }>\n getViewport: (args: { scale: number }) => { width: number; height: number }\n render: (args: { canvasContext: unknown; viewport: { width: number; height: number } }) => { promise: Promise<void> }\n}\n\ntype PdfCanvasFactoryLike = {\n create: (width: number, height: number) => {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array }\n context: unknown\n }\n destroy: (canvasAndContext: {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array } | null\n context: unknown\n }) => void\n}\n\ntype PdfDocumentProxyLike = {\n numPages: number\n canvasFactory: PdfCanvasFactoryLike\n getPage: (pageNumber: number) => Promise<PdfPageProxyLike>\n}\n\nexport type PdfPageOcrInput = {\n pageNumber: number\n extractedText: string | null\n imageBuffer: Buffer | null\n}\n\nexport type PdfOcrPreparationResult = {\n pageCount: number\n pages: PdfPageOcrInput[]\n}\n\nfunction normalizePdfText(rawText: string): string | null {\n const normalized = rawText.replace(/\\s+/g, ' ').trim()\n return normalized.length > 0 ? normalized : null\n}\n\nfunction extractPdfTextContent(items: Array<{ str?: string }> | undefined): string | null {\n if (!Array.isArray(items) || items.length === 0) return null\n const collected = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((value) => value.length > 0)\n .join(' ')\n return normalizePdfText(collected)\n}\n\nfunction resolvePdfRenderScale(page: PdfPageProxyLike): number {\n const baseViewport = page.getViewport({ scale: 1 })\n const baseLargestDimension = Math.max(baseViewport.width, baseViewport.height)\n let scale = Math.min(\n MAX_RENDER_SCALE,\n Math.max(MIN_RENDER_SCALE, MAX_RENDER_DIMENSION / Math.max(baseLargestDimension, 1)),\n )\n\n let viewport = page.getViewport({ scale })\n const pixelCount = viewport.width * viewport.height\n if (pixelCount > MAX_RENDER_PIXELS) {\n scale *= Math.sqrt(MAX_RENDER_PIXELS / pixelCount)\n viewport = page.getViewport({ scale })\n }\n\n return Math.max(1, scale)\n}\n\nasync function renderPdfPageToImageBuffer(\n page: PdfPageProxyLike,\n canvasFactory: PdfCanvasFactoryLike,\n): Promise<Buffer> {\n const viewport = page.getViewport({ scale: resolvePdfRenderScale(page) })\n const canvasAndContext = canvasFactory.create(viewport.width, viewport.height)\n\n try {\n const renderTask = page.render({\n canvasContext: canvasAndContext.context,\n viewport,\n })\n await renderTask.promise\n const imageBuffer = canvasAndContext.canvas.toBuffer('image/png')\n return Buffer.isBuffer(imageBuffer) ? imageBuffer : Buffer.from(imageBuffer)\n } finally {\n canvasFactory.destroy(canvasAndContext)\n }\n}\n\nexport async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPreparationResult> {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl,\n cMapPacked: true,\n standardFontDataUrl,\n })\n\n const pdfDocument = (await loadingTask.promise) as unknown as PdfDocumentProxyLike\n\n try {\n const pages: PdfPageOcrInput[] = []\n\n for (let pageNumber = 1; pageNumber <=
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveMaxOcrPages, resolvePdfPageIterationLimit } from './ocrLimits'\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst cMapUrl = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst standardFontDataUrl = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\nconst logger = createLogger('attachments').child({ component: 'pdf-processing' })\n\nconst MIN_RENDER_SCALE = 2\nconst MAX_RENDER_SCALE = 4\nconst MAX_RENDER_DIMENSION = 2200\nconst MAX_RENDER_PIXELS = 8_000_000\n\ntype PdfPageProxyLike = {\n cleanup: () => void\n getTextContent: () => Promise<{ items?: Array<{ str?: string }> }>\n getViewport: (args: { scale: number }) => { width: number; height: number }\n render: (args: { canvasContext: unknown; viewport: { width: number; height: number } }) => { promise: Promise<void> }\n}\n\ntype PdfCanvasFactoryLike = {\n create: (width: number, height: number) => {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array }\n context: unknown\n }\n destroy: (canvasAndContext: {\n canvas: { toBuffer: (mimeType: string) => Buffer | Uint8Array } | null\n context: unknown\n }) => void\n}\n\ntype PdfDocumentProxyLike = {\n numPages: number\n canvasFactory: PdfCanvasFactoryLike\n getPage: (pageNumber: number) => Promise<PdfPageProxyLike>\n}\n\nexport type PdfPageOcrInput = {\n pageNumber: number\n extractedText: string | null\n imageBuffer: Buffer | null\n}\n\nexport type PdfOcrPreparationResult = {\n pageCount: number\n pages: PdfPageOcrInput[]\n}\n\nfunction normalizePdfText(rawText: string): string | null {\n const normalized = rawText.replace(/\\s+/g, ' ').trim()\n return normalized.length > 0 ? normalized : null\n}\n\nfunction extractPdfTextContent(items: Array<{ str?: string }> | undefined): string | null {\n if (!Array.isArray(items) || items.length === 0) return null\n const collected = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((value) => value.length > 0)\n .join(' ')\n return normalizePdfText(collected)\n}\n\nfunction resolvePdfRenderScale(page: PdfPageProxyLike): number {\n const baseViewport = page.getViewport({ scale: 1 })\n const baseLargestDimension = Math.max(baseViewport.width, baseViewport.height)\n let scale = Math.min(\n MAX_RENDER_SCALE,\n Math.max(MIN_RENDER_SCALE, MAX_RENDER_DIMENSION / Math.max(baseLargestDimension, 1)),\n )\n\n let viewport = page.getViewport({ scale })\n const pixelCount = viewport.width * viewport.height\n if (pixelCount > MAX_RENDER_PIXELS) {\n scale *= Math.sqrt(MAX_RENDER_PIXELS / pixelCount)\n viewport = page.getViewport({ scale })\n }\n\n return Math.max(1, scale)\n}\n\nasync function renderPdfPageToImageBuffer(\n page: PdfPageProxyLike,\n canvasFactory: PdfCanvasFactoryLike,\n): Promise<Buffer> {\n const viewport = page.getViewport({ scale: resolvePdfRenderScale(page) })\n const canvasAndContext = canvasFactory.create(viewport.width, viewport.height)\n\n try {\n const renderTask = page.render({\n canvasContext: canvasAndContext.context,\n viewport,\n })\n await renderTask.promise\n const imageBuffer = canvasAndContext.canvas.toBuffer('image/png')\n return Buffer.isBuffer(imageBuffer) ? imageBuffer : Buffer.from(imageBuffer)\n } finally {\n canvasFactory.destroy(canvasAndContext)\n }\n}\n\nexport async function preparePdfPagesForOcr(filePath: string): Promise<PdfOcrPreparationResult> {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl,\n cMapPacked: true,\n standardFontDataUrl,\n })\n\n const pdfDocument = (await loadingTask.promise) as unknown as PdfDocumentProxyLike\n\n try {\n const pages: PdfPageOcrInput[] = []\n const maxPages = resolveMaxOcrPages()\n const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages)\n if (pdfDocument.numPages > pageLimit) {\n logger.warn('PDF page count exceeds OCR cap; truncating', {\n numPages: pdfDocument.numPages,\n maxPages: pageLimit,\n filePath,\n })\n }\n\n for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n\n try {\n const textContent = await page.getTextContent()\n const extractedText = extractPdfTextContent(textContent.items)\n\n if (extractedText) {\n pages.push({\n pageNumber,\n extractedText,\n imageBuffer: null,\n })\n continue\n }\n\n pages.push({\n pageNumber,\n extractedText: null,\n imageBuffer: await renderPdfPageToImageBuffer(page, pdfDocument.canvasFactory),\n })\n } finally {\n page.cleanup()\n }\n }\n\n return {\n pageCount: pages.length,\n pages,\n }\n } finally {\n await loadingTask.destroy()\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -177,9 +177,11 @@ class ScopedAttachmentUploadService {
|
|
|
177
177
|
throw new ScopedAttachmentUploadError("persistence_failed", 500);
|
|
178
178
|
}
|
|
179
179
|
if (useLlmOcr) {
|
|
180
|
-
|
|
180
|
+
try {
|
|
181
|
+
await requestOcrProcessing(em, attachment, driver, storedPath);
|
|
182
|
+
} catch (error) {
|
|
181
183
|
logger.error("Scoped attachment OCR scheduling failed", { err: error });
|
|
182
|
-
}
|
|
184
|
+
}
|
|
183
185
|
}
|
|
184
186
|
if (this.deps.dataEngine) {
|
|
185
187
|
await emitCrudSideEffects({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/scoped-upload-service.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n requestOcrProcessing(em, attachment, driver, storedPath).catch((error) => {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n })\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from './crud'\nimport type { StorageDriverFactory } from './drivers'\nimport { buildAttachmentFileUrl } from './imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'\nimport { extractAttachmentContent } from './textExtraction'\nimport { requestOcrProcessing } from './ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from './ocrService'\nimport { assertAttachmentScopeInvariant } from './access'\nimport {\n mergeAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from './metadata'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from './security'\nimport { resolveAttachmentMaxBytes } from './upload-limits'\nimport type { AttachmentQuotaService } from './quota-service'\n\nconst logger = createLogger('attachments')\n\nexport type ScopedAttachmentUploadErrorCode =\n | 'dangerous_executable'\n | 'max_upload_size'\n | 'active_content'\n | 'partition_unavailable'\n | 'quota_exceeded'\n | 'quota_target_exists'\n | 'quota_unavailable'\n | 'quota_recovery_unsupported'\n | 'storage_failed'\n | 'persistence_failed'\n\n// Use Symbol.for so the marker survives module duplication across bundle\n// boundaries. The production build emits this module into several server\n// chunks, so a consumer's `instanceof` compares against a different copy of the\n// class than the DI-resolved service threw from: the check silently fails, the\n// error escapes the caller's catch, and a deliberate 400/413 surfaces as a 500.\nconst SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER = Symbol.for('@open-mercato/ScopedAttachmentUploadError')\n\nexport class ScopedAttachmentUploadError extends Error {\n readonly [SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] = true\n\n constructor(\n public readonly code: ScopedAttachmentUploadErrorCode,\n public readonly status: number,\n ) {\n super(code)\n this.name = 'ScopedAttachmentUploadError'\n }\n}\n\n/**\n * Bundle-safe check for `ScopedAttachmentUploadError`. Prefer this over\n * `instanceof` at every call site \u2014 the class is duplicated across server\n * chunks, so `instanceof` is only reliable inside this module.\n */\nexport function isScopedAttachmentUploadError(error: unknown): error is ScopedAttachmentUploadError {\n return Boolean(error)\n && typeof error === 'object'\n && (error as Record<symbol, unknown>)[SCOPED_ATTACHMENT_UPLOAD_ERROR_MARKER] === true\n}\n\nexport type ScopedAttachmentUploadInput = {\n tenantId: string\n organizationId: string\n entityId: string\n recordId: string\n fileName: string\n declaredMimeType?: string | null\n buffer: Buffer\n tags?: string[]\n assignments?: AttachmentAssignment[]\n partitionCode?: string | null\n maxBytes?: number\n /**\n * Reject a partition flagged public, so a module-owned upload (documents,\n * warranty claims) never lands where an anonymous reader can reach it. The\n * tenant/organization half of that guarantee is not this flag's job: the\n * partition lookup below already matches only a global partition or one owned\n * by this tenant and organization, whether or not the flag is set.\n */\n requirePrivatePartition?: boolean\n /**\n * Persists a module-owned link row inside the same transaction as the\n * Attachment, so a link failure cannot leave a committed orphan attachment.\n * The callback receives only the generated id, never the entity or the\n * storage implementation.\n */\n persistLink?: (tx: EntityManager, attachmentId: string) => Promise<void> | void\n}\n\ntype QuotaRecoveryScheduler = (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n) => Promise<void>\n\nexport class ScopedAttachmentUploadService {\n constructor(private readonly deps: {\n em: EntityManager\n dataEngine?: DataEngine | null\n storageDriverFactory: StorageDriverFactory\n attachmentQuotaService: AttachmentQuotaService\n attachmentQuotaRecoveryScheduler: QuotaRecoveryScheduler\n }) {}\n\n async upload(input: ScopedAttachmentUploadInput): Promise<Attachment> {\n const safeName = sanitizeUploadedFileName(input.fileName)\n if (hasDangerousExecutableExtension(safeName)) {\n throw new ScopedAttachmentUploadError('dangerous_executable', 400)\n }\n if (input.buffer.length > (input.maxBytes ?? resolveAttachmentMaxBytes())) {\n throw new ScopedAttachmentUploadError('max_upload_size', 413)\n }\n const mimeType = detectAttachmentMimeType(input.buffer, safeName, input.declaredMimeType ?? '')\n if (isActiveContentAttachment(input.buffer, safeName, mimeType)) {\n throw new ScopedAttachmentUploadError('active_content', 400)\n }\n\n const { em, storageDriverFactory, attachmentQuotaService, attachmentQuotaRecoveryScheduler } = this.deps\n await ensureDefaultPartitions(em)\n const partitionCode = input.partitionCode ?? resolveDefaultPartitionCode(input.entityId)\n const partition = await em.findOne(AttachmentPartition, {\n code: partitionCode,\n $or: [\n { tenantId: null, organizationId: null },\n { tenantId: input.tenantId, organizationId: input.organizationId },\n ],\n })\n if (!partition) throw new ScopedAttachmentUploadError('partition_unavailable', 400)\n if (input.requirePrivatePartition && partition.isPublic) {\n throw new ScopedAttachmentUploadError('partition_unavailable', 403)\n }\n\n const driver = await storageDriverFactory.resolveForPartition(partition.code, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n const preparedStoragePath = driver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n })\n if (!preparedStoragePath) {\n throw new ScopedAttachmentUploadError('quota_recovery_unsupported', 500)\n }\n\n let reservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n try {\n reservation = await attachmentQuotaService.reserve({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n bytes: input.buffer.length,\n source: 'attachment',\n storageDriver: driver.key,\n storagePath: preparedStoragePath,\n partitionCode: partition.code,\n })\n await attachmentQuotaRecoveryScheduler({\n reservationId: reservation.id,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n }, Math.max(1_000, reservation.expiresAt.getTime() - Date.now()))\n await attachmentQuotaService.beginStorage(reservation.id, reservation.leaseToken)\n } catch (error) {\n if (reservation) {\n await attachmentQuotaService.release(reservation.id, reservation.leaseToken).catch(() => undefined)\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') throw new ScopedAttachmentUploadError('quota_exceeded', 413)\n if (code === 'quota_target_exists') throw new ScopedAttachmentUploadError('quota_target_exists', 409)\n throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n }\n\n if (!reservation) throw new ScopedAttachmentUploadError('quota_unavailable', 500)\n\n let storedPath = preparedStoragePath\n try {\n const stored = await driver.store({\n partitionCode: partition.code,\n orgId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n buffer: input.buffer,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n await attachmentQuotaService.markStored(reservation.id, reservation.leaseToken)\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment storage failed', { err: error })\n throw new ScopedAttachmentUploadError('storage_failed', 500)\n }\n\n let extractedContent: string | null = null\n const wantsLlmOcr = partition.requiresOcr && shouldUseLlmOcr(mimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n if (partition.requiresOcr && !useLlmOcr) {\n try {\n const { filePath, cleanup } = await driver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({ filePath, mimeType })\n } finally {\n await cleanup().catch(() => undefined)\n }\n } catch (error) {\n logger.error('Scoped attachment text extraction failed', { err: error })\n }\n }\n\n let assignments = input.assignments?.slice() ?? []\n assignments = upsertAssignment(assignments, { type: input.entityId, id: input.recordId })\n const metadata = mergeAttachmentMetadata(null, {\n assignments,\n tags: input.tags ?? [],\n })\n const attachmentId = randomUUID()\n let attachment!: Attachment\n\n try {\n await em.transactional(async (tx) => {\n attachment = tx.create(Attachment, {\n id: attachmentId,\n entityId: input.entityId,\n recordId: input.recordId,\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n fileName: safeName,\n mimeType,\n fileSize: input.buffer.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n assertAttachmentScopeInvariant({ tenantId: attachment.tenantId, organizationId: attachment.organizationId })\n await tx.persist(attachment).flush()\n await input.persistLink?.(tx, attachmentId)\n await attachmentQuotaService.completeAttachment(reservation.id, reservation.leaseToken, tx)\n })\n } catch (error) {\n await this.compensateStorage(driver, partition.code, storedPath, reservation)\n logger.error('Scoped attachment persistence failed', { err: error })\n // `persistLink` is where callers run their own authorization (the\n // documents module re-checks the edit capability inside this\n // transaction), so a deliberate HTTP error from it must reach the route\n // with its own status. Flattening it to 500 turns a documented 403 into\n // an apparent server fault.\n if (isCrudHttpError(error)) throw error\n throw new ScopedAttachmentUploadError('persistence_failed', 500)\n }\n\n if (useLlmOcr) {\n try {\n await requestOcrProcessing(em, attachment, driver, storedPath)\n } catch (error) {\n logger.error('Scoped attachment OCR scheduling failed', { err: error })\n }\n }\n if (this.deps.dataEngine) {\n await emitCrudSideEffects({\n dataEngine: this.deps.dataEngine,\n action: 'created',\n entity: attachment,\n identifiers: {\n id: attachment.id,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await this.deps.dataEngine.flushOrmEntityChanges()\n }\n return attachment\n }\n\n private async compensateStorage(\n driver: Awaited<ReturnType<StorageDriverFactory['resolveForPartition']>>,\n partitionCode: string,\n storagePath: string,\n reservation: { id: string; leaseToken: string },\n ): Promise<void> {\n try {\n await (driver.deleteStrict?.(partitionCode, storagePath) ?? driver.delete(partitionCode, storagePath))\n await this.deps.attachmentQuotaService.release(reservation.id, reservation.leaseToken)\n } catch (error) {\n logger.error('Scoped attachment compensation failed', { err: error })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { resolveMaxOcrPages, resolvePdfPageIterationLimit } from "./ocrLimits.js";
|
|
6
|
+
const logger = createLogger("attachments").child({ component: "text-extraction" });
|
|
4
7
|
const moduleRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
5
8
|
const pdfJsPackageRoot = path.dirname(moduleRequire.resolve("pdfjs-dist/package.json"));
|
|
6
9
|
const PDF_CMAP_URL = `${path.join(pdfJsPackageRoot, "cmaps")}${path.sep}`;
|
|
@@ -44,8 +47,17 @@ async function extractPdfText(filePath) {
|
|
|
44
47
|
});
|
|
45
48
|
const pdfDocument = await loadingTask.promise;
|
|
46
49
|
const textParts = [];
|
|
50
|
+
const maxPages = resolveMaxOcrPages();
|
|
51
|
+
const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages);
|
|
52
|
+
if (pdfDocument.numPages > pageLimit) {
|
|
53
|
+
logger.warn("PDF page count exceeds text-extraction cap; truncating", {
|
|
54
|
+
numPages: pdfDocument.numPages,
|
|
55
|
+
maxPages: pageLimit,
|
|
56
|
+
filePath
|
|
57
|
+
});
|
|
58
|
+
}
|
|
47
59
|
try {
|
|
48
|
-
for (let pageNumber = 1; pageNumber <=
|
|
60
|
+
for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
|
|
49
61
|
const page = await pdfDocument.getPage(pageNumber);
|
|
50
62
|
try {
|
|
51
63
|
const textContent = await page.getTextContent();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/textExtraction.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\n\n// NOTE: child_process is intentionally NOT imported here.\n// This module MUST NOT shell out to any external binary for content extraction.\n// All extraction must use pure-JS libraries. See HUNT-PARSER-01.\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst PDF_CMAP_URL = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst PDF_STANDARD_FONT_DATA_URL = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\ntype ExtractParams = {\n filePath: string\n mimeType?: string | null\n}\n\nfunction isImage(mimeType?: string | null, filePath?: string | null): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff'].includes(ext)\n }\n return false\n}\n\nfunction isPlainText(mimeType: string, ext: string): boolean {\n return mimeType.startsWith('text/')\n || ext === '.txt'\n || ext === '.md'\n || ext === '.csv'\n || ext === '.log'\n}\n\nfunction isPdf(mimeType: string, ext: string): boolean {\n return mimeType === 'application/pdf' || ext === '.pdf'\n}\n\nfunction isDocx(mimeType: string, ext: string): boolean {\n // mammoth supports only Open XML .docx, not legacy binary .doc files.\n return (\n mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n || ext === '.docx'\n )\n}\n\nasync function extractPlainText(filePath: string): Promise<string | null> {\n try {\n const text = await fs.readFile(filePath, 'utf8')\n const trimmed = text.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nasync function extractPdfText(filePath: string): Promise<string | null> {\n try {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl: PDF_CMAP_URL,\n cMapPacked: true,\n standardFontDataUrl: PDF_STANDARD_FONT_DATA_URL,\n })\n const pdfDocument = await loadingTask.promise\n const textParts: string[] = []\n try {\n for (let pageNumber = 1; pageNumber <=
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import fs from 'fs/promises'\nimport path from 'path'\nimport { createRequire } from 'module'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { resolveMaxOcrPages, resolvePdfPageIterationLimit } from './ocrLimits'\n\nconst logger = createLogger('attachments').child({ component: 'text-extraction' })\n\n// NOTE: child_process is intentionally NOT imported here.\n// This module MUST NOT shell out to any external binary for content extraction.\n// All extraction must use pure-JS libraries. See HUNT-PARSER-01.\n\nconst moduleRequire = createRequire(path.join(process.cwd(), 'package.json'))\nconst pdfJsPackageRoot = path.dirname(moduleRequire.resolve('pdfjs-dist/package.json'))\nconst PDF_CMAP_URL = `${path.join(pdfJsPackageRoot, 'cmaps')}${path.sep}`\nconst PDF_STANDARD_FONT_DATA_URL = `${path.join(pdfJsPackageRoot, 'standard_fonts')}${path.sep}`\n\ntype ExtractParams = {\n filePath: string\n mimeType?: string | null\n}\n\nfunction isImage(mimeType?: string | null, filePath?: string | null): boolean {\n const normalized = (mimeType || '').toLowerCase()\n if (normalized.startsWith('image/')) return true\n if (filePath) {\n const ext = path.extname(filePath).toLowerCase()\n return ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff'].includes(ext)\n }\n return false\n}\n\nfunction isPlainText(mimeType: string, ext: string): boolean {\n return mimeType.startsWith('text/')\n || ext === '.txt'\n || ext === '.md'\n || ext === '.csv'\n || ext === '.log'\n}\n\nfunction isPdf(mimeType: string, ext: string): boolean {\n return mimeType === 'application/pdf' || ext === '.pdf'\n}\n\nfunction isDocx(mimeType: string, ext: string): boolean {\n // mammoth supports only Open XML .docx, not legacy binary .doc files.\n return (\n mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n || ext === '.docx'\n )\n}\n\nasync function extractPlainText(filePath: string): Promise<string | null> {\n try {\n const text = await fs.readFile(filePath, 'utf8')\n const trimmed = text.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nasync function extractPdfText(filePath: string): Promise<string | null> {\n try {\n const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')\n const data = new Uint8Array(await fs.readFile(filePath))\n const loadingTask = getDocument({\n data,\n cMapUrl: PDF_CMAP_URL,\n cMapPacked: true,\n standardFontDataUrl: PDF_STANDARD_FONT_DATA_URL,\n })\n const pdfDocument = await loadingTask.promise\n const textParts: string[] = []\n const maxPages = resolveMaxOcrPages()\n const pageLimit = resolvePdfPageIterationLimit(pdfDocument.numPages, maxPages)\n if (pdfDocument.numPages > pageLimit) {\n logger.warn('PDF page count exceeds text-extraction cap; truncating', {\n numPages: pdfDocument.numPages,\n maxPages: pageLimit,\n filePath,\n })\n }\n try {\n for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {\n const page = await pdfDocument.getPage(pageNumber)\n try {\n const textContent = await page.getTextContent()\n const items = (textContent as any).items as Array<{ str?: string }> | undefined\n if (Array.isArray(items)) {\n const pageText = items\n .map((item) => (typeof item?.str === 'string' ? item.str : ''))\n .filter((s) => s.length > 0)\n .join(' ')\n if (pageText.trim()) textParts.push(pageText.trim())\n }\n } finally {\n page.cleanup()\n }\n }\n } finally {\n await loadingTask.destroy()\n }\n const result = textParts.join('\\n').trim()\n return result.length > 0 ? result : null\n } catch {\n return null\n }\n}\n\nasync function extractDocxText(filePath: string): Promise<string | null> {\n try {\n const mammoth = await import('mammoth')\n const result = await mammoth.extractRawText({ path: filePath })\n const trimmed = result.value.trim()\n return trimmed.length > 0 ? trimmed : null\n } catch {\n return null\n }\n}\n\nexport async function extractAttachmentContent(params: ExtractParams): Promise<string | null> {\n const { filePath, mimeType } = params\n if (!filePath) return null\n if (isImage(mimeType, filePath)) return null\n\n const normalized = (mimeType || '').toLowerCase()\n const ext = path.extname(filePath).toLowerCase()\n\n if (isPlainText(normalized, ext)) {\n return extractPlainText(filePath)\n }\n\n if (isPdf(normalized, ext)) {\n return extractPdfText(filePath)\n }\n\n if (isDocx(normalized, ext)) {\n return extractDocxText(filePath)\n }\n\n // XLSX, PPTX, MSG and other Office formats: no safe pure-JS extractor available yet.\n // Return null rather than shelling out to an external binary.\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -94,10 +94,12 @@ function buildProductPresenter(translate, record) {
|
|
|
94
94
|
) ?? label;
|
|
95
95
|
const isActive = record.is_active ?? record.isActive;
|
|
96
96
|
const statusText = isActive === false ? translate("catalog.search.status.inactive", "Inactive") : null;
|
|
97
|
+
const productType = readRecordText(record, "product_type", "productType");
|
|
98
|
+
const productTypeLabel = productType ? translate(`catalog.products.types.${productType}`, productType) : null;
|
|
97
99
|
const subtitle = formatSubtitle(
|
|
98
100
|
readRecordText(record, "subtitle"),
|
|
99
101
|
readRecordText(record, "sku"),
|
|
100
|
-
|
|
102
|
+
productTypeLabel,
|
|
101
103
|
statusText
|
|
102
104
|
) ?? snippet(readRecordText(record, "description"));
|
|
103
105
|
return { title, subtitle, icon: "package", badge: label };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/catalog/search.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SearchBuildContext, SearchIndexSource, SearchModuleConfig, SearchResultPresenter } from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst CATALOG_PRODUCTS_URL = '/backend/catalog/products'\nconst CATALOG_CATEGORIES_URL = '/backend/catalog/categories'\nconst CATALOG_CONFIG_URL = '/backend/config/catalog'\n\nfunction normalizeText(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n return null\n}\n\nfunction pickText(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n const text = normalizeText(candidate)\n if (text) return text\n }\n return null\n}\n\nfunction readRecordText(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const text = normalizeText(record[key])\n if (text) return text\n }\n return null\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => normalizeText(part))\n .filter((value): value is string => Boolean(value))\n if (!text.length) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction snippet(value: unknown, maxLength = 140): string | undefined {\n const text = normalizeText(value)\n if (!text) return undefined\n if (text.length <= maxLength) return text\n return `${text.slice(0, maxLength - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => normalizeText(item)).filter(Boolean).join(', ')\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_match, firstChar, secondChar) => `${firstChar} ${secondChar}`)\n .replace(/\\b\\w/g, (character) => character.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n appendLine(lines, friendlyLabel(key), value)\n }\n}\n\nfunction buildIndexSource(\n ctx: SearchBuildContext,\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n appendCustomFieldLines(lines, ctx.customFields)\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n}\n\nfunction buildProductUrl(productId: string | null): string | null {\n if (!productId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}`\n}\n\nfunction buildVariantUrl(productId: string | null, variantId: string | null): string | null {\n if (!productId || !variantId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}`\n}\n\nfunction buildCategoryUrl(categoryId: string | null): string | null {\n if (!categoryId) return null\n return `${CATALOG_CATEGORIES_URL}/${encodeURIComponent(categoryId)}/edit`\n}\n\nfunction resolveProductId(record: Record<string, unknown>): string | null {\n // Check direct FK fields first\n const directId = readRecordText(record, 'product_id', 'productId')\n if (directId) return directId\n // Check if product is a string (FK value)\n const product = record.product\n if (typeof product === 'string') return product\n // Check if product is an object with id\n if (product && typeof product === 'object') {\n const productObj = product as Record<string, unknown>\n return readRecordText(productObj, 'id')\n }\n return null\n}\n\nfunction buildProductPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.product', 'Product')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'handle'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'subtitle'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'product_type', 'productType'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'package', badge: label }\n}\n\nfunction buildVariantPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.variant', 'Variant')\n const isDefault = record.is_default ?? record.isDefault\n const defaultLabel = isDefault ? translate('catalog.search.variant.default', 'Default') : null\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'sku'),\n defaultLabel,\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'sku'),\n readRecordText(record, 'barcode'),\n defaultLabel,\n statusText,\n )\n return { title, subtitle, icon: 'box', badge: label }\n}\n\nfunction buildCategoryPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.category', 'Category')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'tree_path', 'treePath'),\n readRecordText(record, 'slug'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'folder-tree', badge: label }\n}\n\nfunction buildOfferPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n channelName?: string | null,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.offer', 'Channel Offer')\n const channelLabel = channelName ?? readRecordText(record, 'channel_name', 'channelName')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'id'),\n ) ?? label\n const titleWithChannel = channelLabel ? `${title} \u00B7 ${channelLabel}` : title\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(statusText) ?? snippet(readRecordText(record, 'description'))\n return { title: titleWithChannel, subtitle, icon: 'store', badge: label }\n}\n\nfunction buildTagPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.tag', 'Tag')\n const title = pickText(\n readRecordText(record, 'label'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const subtitle = formatSubtitle(readRecordText(record, 'slug'))\n return { title, subtitle, icon: 'tag', badge: label }\n}\n\nfunction buildPriceKindPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.priceKind', 'Price Kind')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const displayMode = readRecordText(record, 'display_mode', 'displayMode')\n const isPromotion = record.is_promotion ?? record.isPromotion\n const promotionLabel = isPromotion ? translate('catalog.search.priceKind.promotion', 'Promotion') : null\n const displayModeLabel = displayMode === 'including-tax'\n ? translate('catalog.search.priceKind.includingTax', 'Incl. tax')\n : displayMode === 'excluding-tax'\n ? translate('catalog.search.priceKind.excludingTax', 'Excl. tax')\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n displayModeLabel,\n promotionLabel,\n )\n return { title, subtitle, icon: 'dollar-sign', badge: label }\n}\n\nfunction buildUnitConversionPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.unitConversion', 'Unit Conversion')\n const unitCode = readRecordText(record, 'unit_code', 'unitCode')\n const factor = readRecordText(record, 'to_base_factor', 'toBaseFactor')\n const title = unitCode ? `${unitCode} (\u00D7${factor ?? '?'})` : label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(unitCode, statusText)\n return { title, subtitle, icon: 'ruler', badge: label }\n}\n\nfunction buildOptionSchemaPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.optionSchema', 'Option Schema')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'settings', badge: label }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'catalog:catalog_product',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 10,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Subtitle', record.subtitle)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Handle', record.handle)\n appendLine(lines, 'Product type', record.product_type ?? record.productType)\n appendLine(lines, 'Base unit', record.default_unit ?? record.defaultUnit)\n appendLine(lines, 'Default sales unit', record.default_sales_unit ?? record.defaultSalesUnit)\n appendLine(\n lines,\n 'Default sales quantity',\n record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity,\n )\n appendLine(lines, 'UoM rounding mode', record.uom_rounding_mode ?? record.uomRoundingMode)\n appendLine(lines, 'UoM rounding scale', record.uom_rounding_scale ?? record.uomRoundingScale)\n appendLine(lines, 'Unit price enabled', record.unit_price_enabled ?? record.unitPriceEnabled)\n appendLine(\n lines,\n 'Unit price reference unit',\n record.unit_price_reference_unit ?? record.unitPriceReferenceUnit,\n )\n appendLine(\n lines,\n 'Unit price base quantity',\n record.unit_price_base_quantity ?? record.unitPriceBaseQuantity,\n )\n return buildIndexSource(ctx, buildProductPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildProductPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildProductUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: [\n 'title',\n 'subtitle',\n 'description',\n 'sku',\n 'handle',\n 'product_type',\n 'default_unit',\n 'default_sales_unit',\n 'unit_price_reference_unit',\n ],\n excluded: ['metadata', 'dimensions', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_variant',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 8,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Barcode', record.barcode)\n appendLine(lines, 'Option values', record.option_values ?? record.optionValues)\n return buildIndexSource(ctx, buildVariantPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildVariantPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n const variantId = readRecordText(ctx.record, 'id')\n return buildVariantUrl(productId, variantId)\n },\n fieldPolicy: {\n searchable: ['name', 'sku', 'barcode'],\n excluded: ['metadata', 'dimensions', 'option_values', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_category',\n aclFeatures: ['catalog.categories.view'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Slug', record.slug)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Path', record.tree_path ?? record.treePath)\n return buildIndexSource(ctx, buildCategoryPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildCategoryPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildCategoryUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: ['name', 'slug', 'description', 'tree_path'],\n excluded: ['metadata', 'ancestor_ids', 'child_ids', 'descendant_ids'],\n },\n },\n {\n entityId: 'catalog:catalog_offer',\n aclFeatures: ['sales.channels.manage'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Channel', record.channel_id ?? record.channelId)\n return buildIndexSource(ctx, buildOfferPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOfferPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['title', 'description'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_product_tag',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 5,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Label', record.label)\n appendLine(lines, 'Slug', record.slug)\n return buildIndexSource(ctx, buildTagPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildTagPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_PRODUCTS_URL,\n fieldPolicy: {\n searchable: ['label', 'slug'],\n },\n },\n {\n entityId: 'catalog:catalog_price_kind',\n aclFeatures: ['catalog.settings.manage'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Display mode', record.display_mode ?? record.displayMode)\n appendLine(lines, 'Currency', record.currency_code ?? record.currencyCode)\n return buildIndexSource(ctx, buildPriceKindPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildPriceKindPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['title', 'code'],\n },\n },\n {\n entityId: 'catalog:catalog_product_unit_conversion',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 3,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Unit code', record.unit_code ?? record.unitCode)\n appendLine(lines, 'To base factor', record.to_base_factor ?? record.toBaseFactor)\n appendLine(lines, 'Sort order', record.sort_order ?? record.sortOrder)\n const isActive = record.is_active ?? record.isActive\n appendLine(lines, 'Active', isActive)\n return buildIndexSource(ctx, buildUnitConversionPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildUnitConversionPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['unit_code'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_option_schema_template',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(ctx, buildOptionSchemaPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOptionSchemaPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['name', 'code', 'description'],\n excluded: ['schema', 'metadata'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { SearchBuildContext, SearchIndexSource, SearchModuleConfig, SearchResultPresenter } from '@open-mercato/shared/modules/search'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\n\nconst CATALOG_PRODUCTS_URL = '/backend/catalog/products'\nconst CATALOG_CATEGORIES_URL = '/backend/catalog/categories'\nconst CATALOG_CONFIG_URL = '/backend/config/catalog'\n\nfunction normalizeText(value: unknown): string | null {\n if (value === null || value === undefined) return null\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n }\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n return null\n}\n\nfunction pickText(...candidates: Array<unknown>): string | null {\n for (const candidate of candidates) {\n const text = normalizeText(candidate)\n if (text) return text\n }\n return null\n}\n\nfunction readRecordText(record: Record<string, unknown>, ...keys: string[]): string | null {\n for (const key of keys) {\n const text = normalizeText(record[key])\n if (text) return text\n }\n return null\n}\n\nfunction formatSubtitle(...parts: Array<unknown>): string | undefined {\n const text = parts\n .map((part) => normalizeText(part))\n .filter((value): value is string => Boolean(value))\n if (!text.length) return undefined\n return text.join(' \u00B7 ')\n}\n\nfunction snippet(value: unknown, maxLength = 140): string | undefined {\n const text = normalizeText(value)\n if (!text) return undefined\n if (text.length <= maxLength) return text\n return `${text.slice(0, maxLength - 3)}...`\n}\n\nfunction appendLine(lines: string[], label: string, value: unknown) {\n if (value === null || value === undefined) return\n const text = Array.isArray(value)\n ? value.map((item) => normalizeText(item)).filter(Boolean).join(', ')\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value)\n if (!text.trim()) return\n lines.push(`${label}: ${text}`)\n}\n\nfunction friendlyLabel(input: string): string {\n return input\n .replace(/^cf:/, '')\n .replace(/_/g, ' ')\n .replace(/([a-z])([A-Z])/g, (_match, firstChar, secondChar) => `${firstChar} ${secondChar}`)\n .replace(/\\b\\w/g, (character) => character.toUpperCase())\n}\n\nfunction appendCustomFieldLines(lines: string[], customFields: Record<string, unknown>) {\n for (const [key, value] of Object.entries(customFields)) {\n if (value === null || value === undefined) continue\n appendLine(lines, friendlyLabel(key), value)\n }\n}\n\nfunction buildIndexSource(\n ctx: SearchBuildContext,\n presenter: SearchResultPresenter,\n lines: string[],\n): SearchIndexSource | null {\n appendCustomFieldLines(lines, ctx.customFields)\n if (!lines.length) return null\n return {\n text: lines,\n presenter,\n checksumSource: { record: ctx.record, customFields: ctx.customFields },\n }\n}\n\nfunction buildProductUrl(productId: string | null): string | null {\n if (!productId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}`\n}\n\nfunction buildVariantUrl(productId: string | null, variantId: string | null): string | null {\n if (!productId || !variantId) return null\n return `${CATALOG_PRODUCTS_URL}/${encodeURIComponent(productId)}/variants/${encodeURIComponent(variantId)}`\n}\n\nfunction buildCategoryUrl(categoryId: string | null): string | null {\n if (!categoryId) return null\n return `${CATALOG_CATEGORIES_URL}/${encodeURIComponent(categoryId)}/edit`\n}\n\nfunction resolveProductId(record: Record<string, unknown>): string | null {\n // Check direct FK fields first\n const directId = readRecordText(record, 'product_id', 'productId')\n if (directId) return directId\n // Check if product is a string (FK value)\n const product = record.product\n if (typeof product === 'string') return product\n // Check if product is an object with id\n if (product && typeof product === 'object') {\n const productObj = product as Record<string, unknown>\n return readRecordText(productObj, 'id')\n }\n return null\n}\n\nfunction buildProductPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.product', 'Product')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'sku'),\n readRecordText(record, 'handle'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const productType = readRecordText(record, 'product_type', 'productType')\n const productTypeLabel = productType\n ? translate(`catalog.products.types.${productType}`, productType)\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'subtitle'),\n readRecordText(record, 'sku'),\n productTypeLabel,\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'package', badge: label }\n}\n\nfunction buildVariantPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.variant', 'Variant')\n const isDefault = record.is_default ?? record.isDefault\n const defaultLabel = isDefault ? translate('catalog.search.variant.default', 'Default') : null\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'sku'),\n defaultLabel,\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'sku'),\n readRecordText(record, 'barcode'),\n defaultLabel,\n statusText,\n )\n return { title, subtitle, icon: 'box', badge: label }\n}\n\nfunction buildCategoryPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.category', 'Category')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'tree_path', 'treePath'),\n readRecordText(record, 'slug'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'folder-tree', badge: label }\n}\n\nfunction buildOfferPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n channelName?: string | null,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.offer', 'Channel Offer')\n const channelLabel = channelName ?? readRecordText(record, 'channel_name', 'channelName')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'id'),\n ) ?? label\n const titleWithChannel = channelLabel ? `${title} \u00B7 ${channelLabel}` : title\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(statusText) ?? snippet(readRecordText(record, 'description'))\n return { title: titleWithChannel, subtitle, icon: 'store', badge: label }\n}\n\nfunction buildTagPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.tag', 'Tag')\n const title = pickText(\n readRecordText(record, 'label'),\n readRecordText(record, 'slug'),\n readRecordText(record, 'id'),\n ) ?? label\n const subtitle = formatSubtitle(readRecordText(record, 'slug'))\n return { title, subtitle, icon: 'tag', badge: label }\n}\n\nfunction buildPriceKindPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.priceKind', 'Price Kind')\n const title = pickText(\n readRecordText(record, 'title'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const displayMode = readRecordText(record, 'display_mode', 'displayMode')\n const isPromotion = record.is_promotion ?? record.isPromotion\n const promotionLabel = isPromotion ? translate('catalog.search.priceKind.promotion', 'Promotion') : null\n const displayModeLabel = displayMode === 'including-tax'\n ? translate('catalog.search.priceKind.includingTax', 'Incl. tax')\n : displayMode === 'excluding-tax'\n ? translate('catalog.search.priceKind.excludingTax', 'Excl. tax')\n : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n displayModeLabel,\n promotionLabel,\n )\n return { title, subtitle, icon: 'dollar-sign', badge: label }\n}\n\nfunction buildUnitConversionPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.unitConversion', 'Unit Conversion')\n const unitCode = readRecordText(record, 'unit_code', 'unitCode')\n const factor = readRecordText(record, 'to_base_factor', 'toBaseFactor')\n const title = unitCode ? `${unitCode} (\u00D7${factor ?? '?'})` : label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(unitCode, statusText)\n return { title, subtitle, icon: 'ruler', badge: label }\n}\n\nfunction buildOptionSchemaPresenter(\n translate: TranslateFn,\n record: Record<string, unknown>,\n): SearchResultPresenter {\n const label = translate('catalog.search.badge.optionSchema', 'Option Schema')\n const title = pickText(\n readRecordText(record, 'name'),\n readRecordText(record, 'code'),\n readRecordText(record, 'id'),\n ) ?? label\n const isActive = record.is_active ?? record.isActive\n const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null\n const subtitle = formatSubtitle(\n readRecordText(record, 'code'),\n statusText,\n ) ?? snippet(readRecordText(record, 'description'))\n return { title, subtitle, icon: 'settings', badge: label }\n}\n\nexport const searchConfig: SearchModuleConfig = {\n entities: [\n {\n entityId: 'catalog:catalog_product',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 10,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Subtitle', record.subtitle)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Handle', record.handle)\n appendLine(lines, 'Product type', record.product_type ?? record.productType)\n appendLine(lines, 'Base unit', record.default_unit ?? record.defaultUnit)\n appendLine(lines, 'Default sales unit', record.default_sales_unit ?? record.defaultSalesUnit)\n appendLine(\n lines,\n 'Default sales quantity',\n record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity,\n )\n appendLine(lines, 'UoM rounding mode', record.uom_rounding_mode ?? record.uomRoundingMode)\n appendLine(lines, 'UoM rounding scale', record.uom_rounding_scale ?? record.uomRoundingScale)\n appendLine(lines, 'Unit price enabled', record.unit_price_enabled ?? record.unitPriceEnabled)\n appendLine(\n lines,\n 'Unit price reference unit',\n record.unit_price_reference_unit ?? record.unitPriceReferenceUnit,\n )\n appendLine(\n lines,\n 'Unit price base quantity',\n record.unit_price_base_quantity ?? record.unitPriceBaseQuantity,\n )\n return buildIndexSource(ctx, buildProductPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildProductPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildProductUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: [\n 'title',\n 'subtitle',\n 'description',\n 'sku',\n 'handle',\n 'product_type',\n 'default_unit',\n 'default_sales_unit',\n 'unit_price_reference_unit',\n ],\n excluded: ['metadata', 'dimensions', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_variant',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 8,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'SKU', record.sku)\n appendLine(lines, 'Barcode', record.barcode)\n appendLine(lines, 'Option values', record.option_values ?? record.optionValues)\n return buildIndexSource(ctx, buildVariantPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildVariantPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n const variantId = readRecordText(ctx.record, 'id')\n return buildVariantUrl(productId, variantId)\n },\n fieldPolicy: {\n searchable: ['name', 'sku', 'barcode'],\n excluded: ['metadata', 'dimensions', 'option_values', 'tax_rate_id'],\n },\n },\n {\n entityId: 'catalog:catalog_product_category',\n aclFeatures: ['catalog.categories.view'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Slug', record.slug)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Path', record.tree_path ?? record.treePath)\n return buildIndexSource(ctx, buildCategoryPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildCategoryPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => buildCategoryUrl(readRecordText(ctx.record, 'id')),\n fieldPolicy: {\n searchable: ['name', 'slug', 'description', 'tree_path'],\n excluded: ['metadata', 'ancestor_ids', 'child_ids', 'descendant_ids'],\n },\n },\n {\n entityId: 'catalog:catalog_offer',\n aclFeatures: ['sales.channels.manage'],\n enabled: true,\n priority: 7,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Description', record.description)\n appendLine(lines, 'Channel', record.channel_id ?? record.channelId)\n return buildIndexSource(ctx, buildOfferPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOfferPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['title', 'description'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_product_tag',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 5,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Label', record.label)\n appendLine(lines, 'Slug', record.slug)\n return buildIndexSource(ctx, buildTagPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildTagPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_PRODUCTS_URL,\n fieldPolicy: {\n searchable: ['label', 'slug'],\n },\n },\n {\n entityId: 'catalog:catalog_price_kind',\n aclFeatures: ['catalog.settings.manage'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Title', record.title)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Display mode', record.display_mode ?? record.displayMode)\n appendLine(lines, 'Currency', record.currency_code ?? record.currencyCode)\n return buildIndexSource(ctx, buildPriceKindPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildPriceKindPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['title', 'code'],\n },\n },\n {\n entityId: 'catalog:catalog_product_unit_conversion',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 3,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Unit code', record.unit_code ?? record.unitCode)\n appendLine(lines, 'To base factor', record.to_base_factor ?? record.toBaseFactor)\n appendLine(lines, 'Sort order', record.sort_order ?? record.sortOrder)\n const isActive = record.is_active ?? record.isActive\n appendLine(lines, 'Active', isActive)\n return buildIndexSource(ctx, buildUnitConversionPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildUnitConversionPresenter(translate, ctx.record)\n },\n resolveUrl: async (ctx) => {\n const productId = resolveProductId(ctx.record)\n return buildProductUrl(productId)\n },\n fieldPolicy: {\n searchable: ['unit_code'],\n excluded: ['metadata'],\n },\n },\n {\n entityId: 'catalog:catalog_option_schema_template',\n aclFeatures: ['catalog.products.view'],\n enabled: true,\n priority: 4,\n buildSource: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n const record = ctx.record\n const lines: string[] = []\n appendLine(lines, 'Name', record.name)\n appendLine(lines, 'Code', record.code)\n appendLine(lines, 'Description', record.description)\n return buildIndexSource(ctx, buildOptionSchemaPresenter(translate, record), lines)\n },\n formatResult: async (ctx) => {\n const { t: translate } = await resolveTranslations()\n return buildOptionSchemaPresenter(translate, ctx.record)\n },\n resolveUrl: async () => CATALOG_CONFIG_URL,\n fieldPolicy: {\n searchable: ['name', 'code', 'description'],\n excluded: ['schema', 'metadata'],\n },\n },\n ],\n}\n\nexport default searchConfig\nexport const config = searchConfig\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,2BAA2B;AAEpC,MAAM,uBAAuB;AAC7B,MAAM,yBAAyB;AAC/B,MAAM,qBAAqB;AAE3B,SAAS,cAAc,OAA+B;AACpD,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,WAAO,QAAQ,SAAS,IAAI,UAAU;AAAA,EACxC;AACA,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAChF,SAAO;AACT;AAEA,SAAS,YAAY,YAA2C;AAC9D,aAAW,aAAa,YAAY;AAClC,UAAM,OAAO,cAAc,SAAS;AACpC,QAAI,KAAM,QAAO;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,eAAe,WAAoC,MAA+B;AACzF,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,cAAc,OAAO,GAAG,CAAC;AACtC,QAAI,KAAM,QAAO;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,kBAAkB,OAA2C;AACpE,QAAM,OAAO,MACV,IAAI,CAAC,SAAS,cAAc,IAAI,CAAC,EACjC,OAAO,CAAC,UAA2B,QAAQ,KAAK,CAAC;AACpD,MAAI,CAAC,KAAK,OAAQ,QAAO;AACzB,SAAO,KAAK,KAAK,QAAK;AACxB;AAEA,SAAS,QAAQ,OAAgB,YAAY,KAAyB;AACpE,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,UAAU,UAAW,QAAO;AACrC,SAAO,GAAG,KAAK,MAAM,GAAG,YAAY,CAAC,CAAC;AACxC;AAEA,SAAS,WAAW,OAAiB,OAAe,OAAgB;AAClE,MAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,QAAM,OAAO,MAAM,QAAQ,KAAK,IAC5B,MAAM,IAAI,CAAC,SAAS,cAAc,IAAI,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI,IAClE,OAAO,UAAU,WACf,KAAK,UAAU,KAAK,IACpB,OAAO,KAAK;AAClB,MAAI,CAAC,KAAK,KAAK,EAAG;AAClB,QAAM,KAAK,GAAG,KAAK,KAAK,IAAI,EAAE;AAChC;AAEA,SAAS,cAAc,OAAuB;AAC5C,SAAO,MACJ,QAAQ,QAAQ,EAAE,EAClB,QAAQ,MAAM,GAAG,EACjB,QAAQ,mBAAmB,CAAC,QAAQ,WAAW,eAAe,GAAG,SAAS,IAAI,UAAU,EAAE,EAC1F,QAAQ,SAAS,CAAC,cAAc,UAAU,YAAY,CAAC;AAC5D;AAEA,SAAS,uBAAuB,OAAiB,cAAuC;AACtF,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,QAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,eAAW,OAAO,cAAc,GAAG,GAAG,KAAK;AAAA,EAC7C;AACF;AAEA,SAAS,iBACP,KACA,WACA,OAC0B;AAC1B,yBAAuB,OAAO,IAAI,YAAY;AAC9C,MAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,gBAAgB,EAAE,QAAQ,IAAI,QAAQ,cAAc,IAAI,aAAa;AAAA,EACvE;AACF;AAEA,SAAS,gBAAgB,WAAyC;AAChE,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,GAAG,oBAAoB,IAAI,mBAAmB,SAAS,CAAC;AACjE;AAEA,SAAS,gBAAgB,WAA0B,WAAyC;AAC1F,MAAI,CAAC,aAAa,CAAC,UAAW,QAAO;AACrC,SAAO,GAAG,oBAAoB,IAAI,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,SAAS,CAAC;AAC3G;AAEA,SAAS,iBAAiB,YAA0C;AAClE,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,GAAG,sBAAsB,IAAI,mBAAmB,UAAU,CAAC;AACpE;AAEA,SAAS,iBAAiB,QAAgD;AAExE,QAAM,WAAW,eAAe,QAAQ,cAAc,WAAW;AACjE,MAAI,SAAU,QAAO;AAErB,QAAM,UAAU,OAAO;AACvB,MAAI,OAAO,YAAY,SAAU,QAAO;AAExC,MAAI,WAAW,OAAO,YAAY,UAAU;AAC1C,UAAM,aAAa;AACnB,WAAO,eAAe,YAAY,IAAI;AAAA,EACxC;AACA,SAAO;AACT;AAEA,SAAS,sBACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,gCAAgC,SAAS;AACjE,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,OAAO;AAAA,IAC9B,eAAe,QAAQ,KAAK;AAAA,IAC5B,eAAe,QAAQ,QAAQ;AAAA,IAC/B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,cAAc,eAAe,QAAQ,gBAAgB,aAAa;AACxE,QAAM,mBAAmB,cACrB,UAAU,0BAA0B,WAAW,IAAI,WAAW,IAC9D;AACJ,QAAM,WAAW;AAAA,IACf,eAAe,QAAQ,UAAU;AAAA,IACjC,eAAe,QAAQ,KAAK;AAAA,IAC5B;AAAA,IACA;AAAA,EACF,KAAK,QAAQ,eAAe,QAAQ,aAAa,CAAC;AAClD,SAAO,EAAE,OAAO,UAAU,MAAM,WAAW,OAAO,MAAM;AAC1D;AAEA,SAAS,sBACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,gCAAgC,SAAS;AACjE,QAAM,YAAY,OAAO,cAAc,OAAO;AAC9C,QAAM,eAAe,YAAY,UAAU,kCAAkC,SAAS,IAAI;AAC1F,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,KAAK;AAAA,IAC5B;AAAA,IACA,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,WAAW;AAAA,IACf,eAAe,QAAQ,KAAK;AAAA,IAC5B,eAAe,QAAQ,SAAS;AAAA,IAChC;AAAA,IACA;AAAA,EACF;AACA,SAAO,EAAE,OAAO,UAAU,MAAM,OAAO,OAAO,MAAM;AACtD;AAEA,SAAS,uBACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,iCAAiC,UAAU;AACnE,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,WAAW;AAAA,IACf,eAAe,QAAQ,aAAa,UAAU;AAAA,IAC9C,eAAe,QAAQ,MAAM;AAAA,IAC7B;AAAA,EACF,KAAK,QAAQ,eAAe,QAAQ,aAAa,CAAC;AAClD,SAAO,EAAE,OAAO,UAAU,MAAM,eAAe,OAAO,MAAM;AAC9D;AAEA,SAAS,oBACP,WACA,QACA,aACuB;AACvB,QAAM,QAAQ,UAAU,8BAA8B,eAAe;AACrE,QAAM,eAAe,eAAe,eAAe,QAAQ,gBAAgB,aAAa;AACxF,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,OAAO;AAAA,IAC9B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,mBAAmB,eAAe,GAAG,KAAK,SAAM,YAAY,KAAK;AACvE,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,WAAW,eAAe,UAAU,KAAK,QAAQ,eAAe,QAAQ,aAAa,CAAC;AAC5F,SAAO,EAAE,OAAO,kBAAkB,UAAU,MAAM,SAAS,OAAO,MAAM;AAC1E;AAEA,SAAS,kBACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,4BAA4B,KAAK;AACzD,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,OAAO;AAAA,IAC9B,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,WAAW,eAAe,eAAe,QAAQ,MAAM,CAAC;AAC9D,SAAO,EAAE,OAAO,UAAU,MAAM,OAAO,OAAO,MAAM;AACtD;AAEA,SAAS,wBACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,kCAAkC,YAAY;AACtE,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,OAAO;AAAA,IAC9B,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,cAAc,eAAe,QAAQ,gBAAgB,aAAa;AACxE,QAAM,cAAc,OAAO,gBAAgB,OAAO;AAClD,QAAM,iBAAiB,cAAc,UAAU,sCAAsC,WAAW,IAAI;AACpG,QAAM,mBAAmB,gBAAgB,kBACrC,UAAU,yCAAyC,WAAW,IAC9D,gBAAgB,kBACd,UAAU,yCAAyC,WAAW,IAC9D;AACN,QAAM,WAAW;AAAA,IACf,eAAe,QAAQ,MAAM;AAAA,IAC7B;AAAA,IACA;AAAA,EACF;AACA,SAAO,EAAE,OAAO,UAAU,MAAM,eAAe,OAAO,MAAM;AAC9D;AAEA,SAAS,6BACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,uCAAuC,iBAAiB;AAChF,QAAM,WAAW,eAAe,QAAQ,aAAa,UAAU;AAC/D,QAAM,SAAS,eAAe,QAAQ,kBAAkB,cAAc;AACtE,QAAM,QAAQ,WAAW,GAAG,QAAQ,SAAM,UAAU,GAAG,MAAM;AAC7D,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,WAAW,eAAe,UAAU,UAAU;AACpD,SAAO,EAAE,OAAO,UAAU,MAAM,SAAS,OAAO,MAAM;AACxD;AAEA,SAAS,2BACP,WACA,QACuB;AACvB,QAAM,QAAQ,UAAU,qCAAqC,eAAe;AAC5E,QAAM,QAAQ;AAAA,IACZ,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,MAAM;AAAA,IAC7B,eAAe,QAAQ,IAAI;AAAA,EAC7B,KAAK;AACL,QAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,QAAM,aAAa,aAAa,QAAQ,UAAU,kCAAkC,UAAU,IAAI;AAClG,QAAM,WAAW;AAAA,IACf,eAAe,QAAQ,MAAM;AAAA,IAC7B;AAAA,EACF,KAAK,QAAQ,eAAe,QAAQ,aAAa,CAAC;AAClD,SAAO,EAAE,OAAO,UAAU,MAAM,YAAY,OAAO,MAAM;AAC3D;AAEO,MAAM,eAAmC;AAAA,EAC9C,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,SAAS,OAAO,KAAK;AACvC,mBAAW,OAAO,YAAY,OAAO,QAAQ;AAC7C,mBAAW,OAAO,eAAe,OAAO,WAAW;AACnD,mBAAW,OAAO,OAAO,OAAO,GAAG;AACnC,mBAAW,OAAO,UAAU,OAAO,MAAM;AACzC,mBAAW,OAAO,gBAAgB,OAAO,gBAAgB,OAAO,WAAW;AAC3E,mBAAW,OAAO,aAAa,OAAO,gBAAgB,OAAO,WAAW;AACxE,mBAAW,OAAO,sBAAsB,OAAO,sBAAsB,OAAO,gBAAgB;AAC5F;AAAA,UACE;AAAA,UACA;AAAA,UACA,OAAO,+BAA+B,OAAO;AAAA,QAC/C;AACA,mBAAW,OAAO,qBAAqB,OAAO,qBAAqB,OAAO,eAAe;AACzF,mBAAW,OAAO,sBAAsB,OAAO,sBAAsB,OAAO,gBAAgB;AAC5F,mBAAW,OAAO,sBAAsB,OAAO,sBAAsB,OAAO,gBAAgB;AAC5F;AAAA,UACE;AAAA,UACA;AAAA,UACA,OAAO,6BAA6B,OAAO;AAAA,QAC7C;AACA;AAAA,UACE;AAAA,UACA;AAAA,UACA,OAAO,4BAA4B,OAAO;AAAA,QAC5C;AACA,eAAO,iBAAiB,KAAK,sBAAsB,WAAW,MAAM,GAAG,KAAK;AAAA,MAC9E;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,sBAAsB,WAAW,IAAI,MAAM;AAAA,MACpD;AAAA,MACA,YAAY,OAAO,QAAQ,gBAAgB,eAAe,IAAI,QAAQ,IAAI,CAAC;AAAA,MAC3E,aAAa;AAAA,QACX,YAAY;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,UAAU,CAAC,YAAY,cAAc,aAAa;AAAA,MACpD;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,OAAO,OAAO,GAAG;AACnC,mBAAW,OAAO,WAAW,OAAO,OAAO;AAC3C,mBAAW,OAAO,iBAAiB,OAAO,iBAAiB,OAAO,YAAY;AAC9E,eAAO,iBAAiB,KAAK,sBAAsB,WAAW,MAAM,GAAG,KAAK;AAAA,MAC9E;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,sBAAsB,WAAW,IAAI,MAAM;AAAA,MACpD;AAAA,MACA,YAAY,OAAO,QAAQ;AACzB,cAAM,YAAY,iBAAiB,IAAI,MAAM;AAC7C,cAAM,YAAY,eAAe,IAAI,QAAQ,IAAI;AACjD,eAAO,gBAAgB,WAAW,SAAS;AAAA,MAC7C;AAAA,MACA,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,OAAO,SAAS;AAAA,QACrC,UAAU,CAAC,YAAY,cAAc,iBAAiB,aAAa;AAAA,MACrE;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,yBAAyB;AAAA,MACvC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,eAAe,OAAO,WAAW;AACnD,mBAAW,OAAO,QAAQ,OAAO,aAAa,OAAO,QAAQ;AAC7D,eAAO,iBAAiB,KAAK,uBAAuB,WAAW,MAAM,GAAG,KAAK;AAAA,MAC/E;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,uBAAuB,WAAW,IAAI,MAAM;AAAA,MACrD;AAAA,MACA,YAAY,OAAO,QAAQ,iBAAiB,eAAe,IAAI,QAAQ,IAAI,CAAC;AAAA,MAC5E,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,QAAQ,eAAe,WAAW;AAAA,QACvD,UAAU,CAAC,YAAY,gBAAgB,aAAa,gBAAgB;AAAA,MACtE;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,SAAS,OAAO,KAAK;AACvC,mBAAW,OAAO,eAAe,OAAO,WAAW;AACnD,mBAAW,OAAO,WAAW,OAAO,cAAc,OAAO,SAAS;AAClE,eAAO,iBAAiB,KAAK,oBAAoB,WAAW,MAAM,GAAG,KAAK;AAAA,MAC5E;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,oBAAoB,WAAW,IAAI,MAAM;AAAA,MAClD;AAAA,MACA,YAAY,OAAO,QAAQ;AACzB,cAAM,YAAY,iBAAiB,IAAI,MAAM;AAC7C,eAAO,gBAAgB,SAAS;AAAA,MAClC;AAAA,MACA,aAAa;AAAA,QACX,YAAY,CAAC,SAAS,aAAa;AAAA,QACnC,UAAU,CAAC,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,SAAS,OAAO,KAAK;AACvC,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,eAAO,iBAAiB,KAAK,kBAAkB,WAAW,MAAM,GAAG,KAAK;AAAA,MAC1E;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,kBAAkB,WAAW,IAAI,MAAM;AAAA,MAChD;AAAA,MACA,YAAY,YAAY;AAAA,MACxB,aAAa;AAAA,QACX,YAAY,CAAC,SAAS,MAAM;AAAA,MAC9B;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,yBAAyB;AAAA,MACvC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,SAAS,OAAO,KAAK;AACvC,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,gBAAgB,OAAO,gBAAgB,OAAO,WAAW;AAC3E,mBAAW,OAAO,YAAY,OAAO,iBAAiB,OAAO,YAAY;AACzE,eAAO,iBAAiB,KAAK,wBAAwB,WAAW,MAAM,GAAG,KAAK;AAAA,MAChF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,wBAAwB,WAAW,IAAI,MAAM;AAAA,MACtD;AAAA,MACA,YAAY,YAAY;AAAA,MACxB,aAAa;AAAA,QACX,YAAY,CAAC,SAAS,MAAM;AAAA,MAC9B;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,aAAa,OAAO,aAAa,OAAO,QAAQ;AAClE,mBAAW,OAAO,kBAAkB,OAAO,kBAAkB,OAAO,YAAY;AAChF,mBAAW,OAAO,cAAc,OAAO,cAAc,OAAO,SAAS;AACrE,cAAM,WAAW,OAAO,aAAa,OAAO;AAC5C,mBAAW,OAAO,UAAU,QAAQ;AACpC,eAAO,iBAAiB,KAAK,6BAA6B,WAAW,MAAM,GAAG,KAAK;AAAA,MACrF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,6BAA6B,WAAW,IAAI,MAAM;AAAA,MAC3D;AAAA,MACA,YAAY,OAAO,QAAQ;AACzB,cAAM,YAAY,iBAAiB,IAAI,MAAM;AAC7C,eAAO,gBAAgB,SAAS;AAAA,MAClC;AAAA,MACA,aAAa;AAAA,QACX,YAAY,CAAC,WAAW;AAAA,QACxB,UAAU,CAAC,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,aAAa,CAAC,uBAAuB;AAAA,MACrC,SAAS;AAAA,MACT,UAAU;AAAA,MACV,aAAa,OAAO,QAAQ;AAC1B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,cAAM,SAAS,IAAI;AACnB,cAAM,QAAkB,CAAC;AACzB,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,QAAQ,OAAO,IAAI;AACrC,mBAAW,OAAO,eAAe,OAAO,WAAW;AACnD,eAAO,iBAAiB,KAAK,2BAA2B,WAAW,MAAM,GAAG,KAAK;AAAA,MACnF;AAAA,MACA,cAAc,OAAO,QAAQ;AAC3B,cAAM,EAAE,GAAG,UAAU,IAAI,MAAM,oBAAoB;AACnD,eAAO,2BAA2B,WAAW,IAAI,MAAM;AAAA,MACzD;AAAA,MACA,YAAY,YAAY;AAAA,MACxB,aAAa;AAAA,QACX,YAAY,CAAC,QAAQ,QAAQ,aAAa;AAAA,QAC1C,UAAU,CAAC,UAAU,UAAU;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;AACR,MAAM,SAAS;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
OAuthStateError,
|
|
14
14
|
verifyOAuthState
|
|
15
15
|
} from "../../../../../lib/oauth-state.js";
|
|
16
|
+
import { OAUTH_RESULT_QUERY_PARAM } from "../../../../../lib/oauth-result.js";
|
|
16
17
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
17
18
|
const logger = createLogger("communication_channels").child({ component: "oauth-callback" });
|
|
18
19
|
const metadata = {
|
|
@@ -26,7 +27,7 @@ function redirectWithFlash(req, returnUrl, flash) {
|
|
|
26
27
|
normalizeOAuthReturnUrl(returnUrl, DEFAULT_OAUTH_RETURN_URL),
|
|
27
28
|
getAppBaseUrl(req)
|
|
28
29
|
);
|
|
29
|
-
base.searchParams.set(
|
|
30
|
+
base.searchParams.set(OAUTH_RESULT_QUERY_PARAM, flash.type);
|
|
30
31
|
if (flash.code) base.searchParams.set("code", flash.code);
|
|
31
32
|
if (flash.provider) base.searchParams.set("provider", flash.provider);
|
|
32
33
|
if (flash.channelId) base.searchParams.set("channelId", flash.channelId);
|