@open-mercato/core 0.6.2-develop.3381.1.8461b312a9 → 0.6.2-develop.3400.1.d948268f50
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/file/[id]/route.js +8 -8
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -8
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +6 -2
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/route.js +2 -0
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/openapi.js +5 -1
- package/dist/modules/attachments/api/openapi.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +10 -1
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +19 -7
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/backend/config/attachments/page.js +3 -1
- package/dist/modules/attachments/backend/config/attachments/page.js.map +2 -2
- package/dist/modules/attachments/cli.js +6 -2
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +216 -10
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/attachments/di.js +13 -0
- package/dist/modules/attachments/di.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/driverFactory.js +67 -0
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/index.js +11 -0
- package/dist/modules/attachments/lib/drivers/index.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js +42 -0
- package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/localDriver.js +70 -0
- package/dist/modules/attachments/lib/drivers/localDriver.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/types.js +1 -0
- package/dist/modules/attachments/lib/drivers/types.js.map +7 -0
- package/dist/modules/attachments/lib/ocrQueue.js +10 -5
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/storage.js.map +2 -2
- package/dist/modules/attachments/lib/thumbnailCache.js +11 -13
- package/dist/modules/attachments/lib/thumbnailCache.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +31 -5
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +14 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/package.json +11 -7
- package/src/modules/attachments/api/file/[id]/route.ts +10 -8
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +8 -8
- package/src/modules/attachments/api/library/[id]/route.ts +7 -3
- package/src/modules/attachments/api/library/route.ts +2 -0
- package/src/modules/attachments/api/openapi.ts +4 -0
- package/src/modules/attachments/api/partitions/route.ts +10 -1
- package/src/modules/attachments/api/route.ts +20 -7
- package/src/modules/attachments/backend/config/attachments/page.tsx +3 -1
- package/src/modules/attachments/cli.ts +7 -2
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +275 -7
- package/src/modules/attachments/di.ts +13 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +91 -0
- package/src/modules/attachments/lib/drivers/index.ts +4 -0
- package/src/modules/attachments/lib/drivers/legacyPublicDriver.ts +46 -0
- package/src/modules/attachments/lib/drivers/localDriver.ts +79 -0
- package/src/modules/attachments/lib/drivers/types.ts +28 -0
- package/src/modules/attachments/lib/ocrQueue.ts +14 -6
- package/src/modules/attachments/lib/storage.ts +13 -0
- package/src/modules/attachments/lib/thumbnailCache.ts +25 -19
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +30 -4
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +15 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/api/partitions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await em.find(AttachmentPartition, {}, { orderBy: { createdAt: 'asc' } })\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await em.findOne(AttachmentPartition, { code })\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: 'local',\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usage = await em.count(Attachment, { partitionCode: entry.code })\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await em.find(AttachmentPartition, {}, { orderBy: { createdAt: 'asc' } })\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await em.findOne(AttachmentPartition, { code })\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await em.findOne(AttachmentPartition, { id: parsed.data.id })\n if (!entry) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usage = await em.count(Attachment, { partitionCode: entry.code })\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,9 +6,9 @@ import { sql } from "kysely";
|
|
|
6
6
|
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "../lib/imageUrls.js";
|
|
7
7
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from "../lib/partitions.js";
|
|
8
8
|
import { Attachment, AttachmentPartition } from "../data/entities.js";
|
|
9
|
-
import { storePartitionFile, deletePartitionFile } from "../lib/storage.js";
|
|
10
9
|
import { extractAttachmentContent } from "../lib/textExtraction.js";
|
|
11
10
|
import { requestOcrProcessing } from "../lib/ocrQueue.js";
|
|
11
|
+
import { StorageDriverFactory } from "../lib/drivers/index.js";
|
|
12
12
|
import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
|
|
13
13
|
import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
|
|
14
14
|
import {
|
|
@@ -256,6 +256,7 @@ async function POST(req) {
|
|
|
256
256
|
const { resolve } = await createRequestContainer();
|
|
257
257
|
const em = resolve("em");
|
|
258
258
|
const dataEngine = resolve("dataEngine");
|
|
259
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
259
260
|
await ensureDefaultPartitions(em);
|
|
260
261
|
let partitionFromField = null;
|
|
261
262
|
let fieldMaxAttachmentSizeMb = null;
|
|
@@ -335,15 +336,17 @@ async function POST(req) {
|
|
|
335
336
|
if (requestedPublicOverride) {
|
|
336
337
|
return NextResponse.json({ error: t("attachments.errors.publicPartitionBlocked", "Public storage partitions cannot be selected explicitly for this upload.") }, { status: 403 });
|
|
337
338
|
}
|
|
338
|
-
|
|
339
|
+
const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId });
|
|
340
|
+
let storedPath;
|
|
339
341
|
try {
|
|
340
|
-
stored = await
|
|
342
|
+
const stored = await uploadDriver.store({
|
|
341
343
|
partitionCode: partition.code,
|
|
342
344
|
orgId,
|
|
343
345
|
tenantId,
|
|
344
346
|
fileName: safeName,
|
|
345
347
|
buffer: buf
|
|
346
348
|
});
|
|
349
|
+
storedPath = stored.storagePath;
|
|
347
350
|
} catch (error) {
|
|
348
351
|
console.error("[attachments] failed to persist file", error);
|
|
349
352
|
return NextResponse.json({ error: "Failed to persist attachment." }, { status: 500 });
|
|
@@ -354,13 +357,17 @@ async function POST(req) {
|
|
|
354
357
|
const ocrService = wantsLlmOcr ? new OcrService() : null;
|
|
355
358
|
const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available);
|
|
356
359
|
if (requiresOcr && !useLlmOcr) {
|
|
360
|
+
const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath);
|
|
357
361
|
try {
|
|
358
362
|
extractedContent = await extractAttachmentContent({
|
|
359
|
-
filePath:
|
|
363
|
+
filePath: localPath,
|
|
360
364
|
mimeType: fileMimeType
|
|
361
365
|
});
|
|
362
366
|
} catch (error) {
|
|
363
367
|
console.error("[attachments] failed to extract attachment content", error);
|
|
368
|
+
} finally {
|
|
369
|
+
await cleanup().catch(() => {
|
|
370
|
+
});
|
|
364
371
|
}
|
|
365
372
|
}
|
|
366
373
|
let assignments = assignmentsFromForm.slice();
|
|
@@ -380,14 +387,14 @@ async function POST(req) {
|
|
|
380
387
|
fileSize: buf.length,
|
|
381
388
|
partitionCode: partition.code,
|
|
382
389
|
storageDriver: partition.storageDriver || "local",
|
|
383
|
-
storagePath:
|
|
390
|
+
storagePath: storedPath,
|
|
384
391
|
url: buildAttachmentFileUrl(attachmentId),
|
|
385
392
|
content: extractedContent,
|
|
386
393
|
storageMetadata: metadata2
|
|
387
394
|
});
|
|
388
395
|
await em.persist(att).flush();
|
|
389
396
|
if (useLlmOcr) {
|
|
390
|
-
requestOcrProcessing(em, att,
|
|
397
|
+
requestOcrProcessing(em, att, uploadDriver, storedPath).catch((error) => {
|
|
391
398
|
console.error("[attachments] failed to queue OCR processing", error);
|
|
392
399
|
});
|
|
393
400
|
} else if (wantsLlmOcr) {
|
|
@@ -465,6 +472,7 @@ async function DELETE(req) {
|
|
|
465
472
|
const { resolve } = await createRequestContainer();
|
|
466
473
|
const em = resolve("em");
|
|
467
474
|
const dataEngine = resolve("dataEngine");
|
|
475
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
468
476
|
const deleteFilter = { id, tenantId: auth.tenantId, organizationId: auth.orgId };
|
|
469
477
|
const record = await em.findOne(Attachment, deleteFilter);
|
|
470
478
|
if (!record) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
|
|
@@ -473,7 +481,11 @@ async function DELETE(req) {
|
|
|
473
481
|
console.error("[attachments] failed to cleanup cached thumbnails", error);
|
|
474
482
|
});
|
|
475
483
|
if (record.storagePath) {
|
|
476
|
-
await
|
|
484
|
+
const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
485
|
+
tenantId: record.tenantId ?? auth.tenantId,
|
|
486
|
+
organizationId: record.organizationId ?? auth.orgId
|
|
487
|
+
});
|
|
488
|
+
await delDriver.delete(record.partitionCode, record.storagePath);
|
|
477
489
|
}
|
|
478
490
|
if (dataEngine) {
|
|
479
491
|
await emitCrudSideEffects({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/api/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { storePartitionFile, deletePartitionFile } from '../lib/storage'\nimport { extractAttachmentContent } from '../lib/textExtraction'\nimport { requestOcrProcessing } from '../lib/ocrQueue'\nimport { OcrService, shouldUseLlmOcr } from '../lib/ocrService'\nimport { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from '../lib/metadata'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from '../lib/security'\nimport {\n isMultipartRequestWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '../lib/upload-limits'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\nconst attachmentQuerySchema = z.object({\n entityId: z.string().min(1).describe('Entity identifier that owns the attachments'),\n recordId: z.string().min(1).describe('Record identifier within the entity'),\n page: z.coerce.number().min(1).optional(),\n pageSize: z.coerce.number().min(1).max(100).optional(),\n})\n\nconst attachmentAssignmentSchema = z.object({\n type: z.string().describe('Assignment type identifier'),\n id: z.string().describe('Assignment record identifier'),\n href: z.string().nullable().optional().describe('Optional link to the related record'),\n label: z.string().nullable().optional().describe('Optional label for the assignment'),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string().describe('Attachment identifier'),\n url: z.string().describe('Public path to the stored asset'),\n fileName: z.string().describe('Original filename'),\n fileSize: z.number().int().nonnegative().describe('File size in bytes'),\n createdAt: z.string().describe('Upload timestamp (ISO 8601)'),\n mimeType: z.string().nullable().optional().describe('MIME type of the file'),\n thumbnailUrl: z.string().optional().describe('Helper route that renders a thumbnail'),\n partitionCode: z.string().optional().describe('Partition identifier'),\n tags: z.array(z.string()).optional().describe('Tags assigned to the attachment'),\n content: z.string().nullable().optional().describe('Extracted text or markdown content'),\n assignments: z.array(attachmentAssignmentSchema).optional().describe('Records that reference this attachment'),\n})\n\nconst attachmentListResponseSchema = z.object({\n items: z.array(attachmentItemSchema),\n total: z.number().int().nonnegative().optional(),\n page: z.number().int().min(1).optional(),\n pageSize: z.number().int().min(1).optional(),\n totalPages: z.number().int().min(1).optional(),\n})\n\nconst attachmentUploadBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n fieldKey: z.string().optional(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n customFields: z\n .string()\n .optional()\n .describe('JSON encoded map of custom field values collected from the upload form.'),\n})\n\nconst attachmentDeleteQuerySchema = z.object({\n id: z.string().uuid(),\n})\n\nconst uploadResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string(),\n url: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n thumbnailUrl: z.string().optional(),\n content: z.string().nullable().optional(),\n tags: z.array(z.string()).optional(),\n assignments: z.array(attachmentAssignmentSchema).optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n }),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction parseCustomFieldsEntry(value: FormDataEntryValue | null): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return {}\n try {\n const parsed = JSON.parse(trimmed)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n } catch {\n return {}\n }\n }\n if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n return { ...(value as Record<string, unknown>) }\n }\n return {}\n}\n\nfunction buildFormPayload(form: FormData): Record<string, unknown> {\n const payload: Record<string, unknown> = {}\n form.forEach((value, key) => {\n if (key === 'customFields') {\n payload.customFields = parseCustomFieldsEntry(value)\n return\n }\n payload[key] = value\n })\n return payload\n}\n\nfunction parseFormTags(value: FormDataEntryValue | null): string[] {\n if (!value) return []\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentTags(parsed)\n } catch {\n return normalizeAttachmentTags(value)\n }\n }\n return []\n}\n\nfunction parseFormAssignments(value: FormDataEntryValue | null): AttachmentAssignment[] {\n if (!value) return []\n if (typeof value !== 'string') return []\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentAssignments(parsed)\n } catch {\n return []\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsedQuery = attachmentQuerySchema.safeParse({\n entityId: url.searchParams.get('entityId') || '',\n recordId: url.searchParams.get('recordId') || '',\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'entityId and recordId are required' }, { status: 400 })\n }\n const { entityId, recordId, page, pageSize } = parsedQuery.data\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const filter: Record<string, unknown> = { entityId, recordId, tenantId: auth.tenantId! }\n if (auth.orgId) filter.organizationId = auth.orgId\n const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC' }\n const usePaging = typeof page === 'number' && typeof pageSize === 'number'\n const total = usePaging ? await em.count(Attachment, filter) : null\n const currentPage = usePaging ? Math.max(1, page) : null\n const currentPageSize = usePaging ? pageSize : null\n const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null\n const pageOffset = usePaging ? (Math.min(currentPage!, totalPages!) - 1) * currentPageSize! : undefined\n const items = await findWithDecryption(\n em,\n Attachment,\n filter,\n {\n orderBy,\n ...(usePaging\n ? {\n limit: currentPageSize!,\n offset: pageOffset,\n }\n : {}),\n },\n {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n },\n )\n return NextResponse.json({\n items: items.map((a: any) => {\n const metadata = readAttachmentMetadata(a.storageMetadata)\n return {\n id: a.id,\n url: a.url,\n fileName: a.fileName,\n fileSize: a.fileSize,\n createdAt: a.createdAt,\n mimeType: a.mimeType ?? null,\n partitionCode: a.partitionCode,\n content: a.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(a.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(a.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n }\n }),\n ...(usePaging\n ? {\n total,\n page: Math.min(currentPage!, totalPages!),\n pageSize: currentPageSize,\n totalPages,\n }\n : {}),\n })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const tenantId = auth.tenantId\n const orgId = auth.orgId\n\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ error: 'Expected multipart/form-data' }, { status: 400 })\n }\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({ error: 'Attachment exceeds the maximum upload size.' }, { status: 413 })\n }\n\n const form = await req.formData()\n const formPayload = buildFormPayload(form)\n const customFieldValues = splitCustomFieldPayload(formPayload).custom\n const entityId = String(form.get('entityId') || '')\n const recordId = String(form.get('recordId') || '')\n const fieldKey = String(form.get('fieldKey') || '')\n const file = form.get('file') as unknown as File | null\n if (!entityId || !recordId || !file) return NextResponse.json({ error: 'entityId, recordId and file are required' }, { status: 400 })\n const partitionOverrideRaw = form.get('partitionCode')\n const partitionOverride =\n typeof partitionOverrideRaw === 'string' && partitionOverrideRaw.trim().length > 0\n ? sanitizePartitionCode(partitionOverrideRaw)\n : null\n const tags = parseFormTags(form.get('tags'))\n const assignmentsFromForm = parseFormAssignments(form.get('assignments'))\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine')\n await ensureDefaultPartitions(em)\n // Optional per-field validations\n let partitionFromField: string | null = null\n let fieldMaxAttachmentSizeMb: number | null = null\n if (fieldKey) {\n try {\n const { CustomFieldDef } = await import('@open-mercato/core/modules/entities/data/entities')\n const def = await em.findOne(CustomFieldDef, {\n entityId,\n key: fieldKey,\n $and: [\n { $or: [ { tenantId: auth.tenantId }, { tenantId: null } ] },\n ],\n isActive: true,\n })\n const cfg = (def as any)?.configJson || {}\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n if (Array.isArray(cfg.acceptExtensions) && cfg.acceptExtensions.length) {\n const allowed = new Set((cfg.acceptExtensions as any[]).map((x: any) => String(x).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) return NextResponse.json({ error: 'File type not allowed' }, { status: 400 })\n }\n if (typeof cfg.maxAttachmentSizeMb === 'number' && cfg.maxAttachmentSizeMb > 0) {\n fieldMaxAttachmentSizeMb = cfg.maxAttachmentSizeMb\n }\n if (typeof cfg.partitionCode === 'string' && cfg.partitionCode.trim().length > 0) {\n partitionFromField = sanitizePartitionCode(cfg.partitionCode)\n }\n } catch {}\n }\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({\n error: t('attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'),\n }, { status: 400 })\n }\n const effectiveMaxBytes = resolveAttachmentMaxBytes(fieldMaxAttachmentSizeMb)\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n const buf = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeUploadedFileName(file.name)\n const fileMimeType = detectAttachmentMimeType(buf, safeName, (file as any).type)\n if (isActiveContentAttachment(buf, safeName, fileMimeType)) {\n return NextResponse.json({ error: t('attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n const defaultPartitionCode = resolveDefaultPartitionCode(entityId)\n const resolvedPartitionCode = partitionOverride ?? partitionFromField ?? defaultPartitionCode\n const partitionCodeCandidates = Array.from(\n new Set(\n [partitionOverride, partitionFromField, resolvedPartitionCode].filter(\n (code): code is string => typeof code === 'string' && code.length > 0,\n ),\n ),\n )\n let partition: AttachmentPartition | null = null\n for (const code of partitionCodeCandidates) {\n const record = await em.findOne(AttachmentPartition, { code })\n if (record) {\n partition = record\n break\n }\n }\n if (!partition) {\n partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode })\n }\n if (!partition) {\n return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })\n }\n const requestedPublicOverride =\n typeof partitionOverride === 'string' &&\n partitionOverride.length > 0 &&\n partition.code === partitionOverride &&\n partition.isPublic === true &&\n partition.code !== defaultPartitionCode &&\n partition.code !== partitionFromField\n if (requestedPublicOverride) {\n return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })\n }\n let stored\n try {\n stored = await storePartitionFile({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n buffer: buf,\n })\n } catch (error) {\n console.error('[attachments] failed to persist file', error)\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n const requiresOcr =\n typeof (partition as any).requiresOcr === 'boolean'\n ? Boolean((partition as any).requiresOcr)\n : resolveDefaultAttachmentOcrEnabled()\n let extractedContent: string | null = null\n const wantsLlmOcr = requiresOcr && shouldUseLlmOcr(fileMimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n\n if (requiresOcr && !useLlmOcr) {\n try {\n extractedContent = await extractAttachmentContent({\n filePath: stored.absolutePath,\n mimeType: fileMimeType,\n })\n } catch (error) {\n console.error('[attachments] failed to extract attachment content', error)\n }\n }\n\n let assignments = assignmentsFromForm.slice()\n if (entityId !== LIBRARY_ENTITY_ID) {\n assignments = upsertAssignment(assignments, { type: entityId, id: recordId })\n }\n const metadata = mergeAttachmentMetadata(null, { assignments, tags })\n const attachmentId = randomUUID()\n const att = em.create(Attachment, {\n id: attachmentId,\n entityId,\n recordId,\n organizationId: auth.orgId!,\n tenantId: auth.tenantId!,\n fileName: safeName,\n mimeType: fileMimeType,\n fileSize: buf.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: stored.storagePath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n await em.persist(att).flush()\n\n if (useLlmOcr) {\n requestOcrProcessing(em, att, stored.absolutePath).catch((error) => {\n console.error('[attachments] failed to queue OCR processing', error)\n })\n } else if (wantsLlmOcr) {\n console.warn('[attachments] OCR requested but OPENAI_API_KEY not configured, falling back to text extraction when available')\n }\n\n if (dataEngine) {\n try {\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.attachments.attachment,\n recordId: attachmentId,\n tenantId,\n organizationId: orgId,\n values: customFieldValues,\n })\n } catch (error) {\n console.error('[attachments] failed to persist custom attributes', error)\n return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })\n }\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: att,\n identifiers: {\n id: att.id,\n organizationId: att.organizationId ?? null,\n tenantId: att.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({\n ok: true,\n item: {\n id: attachmentId,\n url: att.url,\n fileName: safeName,\n fileSize: buf.length,\n partitionCode: partition.code,\n thumbnailUrl: buildAttachmentImageUrl(attachmentId, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(safeName),\n }),\n content: extractedContent ?? null,\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n customFields: Object.keys(customFieldValues).length ? customFieldValues : undefined,\n },\n })\n}\n\nasync function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: string): Promise<number> {\n try {\n const db = em.getKysely<any>() as any\n const row = await db\n .selectFrom('attachments')\n .select(sql<string>`sum(file_size)`.as('total_size'))\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst() as { total_size: string | number | null } | undefined\n const total = row?.total_size\n if (typeof total === 'number') return Number.isFinite(total) ? total : 0\n if (typeof total === 'string') {\n const parsed = Number(total)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n } catch {\n return 0\n }\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const id = url.searchParams.get('id') || ''\n if (!id) return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine')\n const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId!, organizationId: auth.orgId }\n const record = await em.findOne(Attachment, deleteFilter)\n if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n await em.remove(record).flush()\n await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {\n console.error('[attachments] failed to cleanup cached thumbnails', error)\n })\n if (record.storagePath) {\n await deletePartitionFile(record.partitionCode, record.storagePath, record.storageDriver)\n }\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? null,\n tenantId: record.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage entity attachments',\n description: 'Upload and list attachments associated with module entities and records.',\n methods: {\n GET: {\n summary: 'List attachments for a record',\n description: 'Returns uploaded attachments for the given entity record, ordered by newest first.',\n query: attachmentQuerySchema,\n responses: [\n { status: 200, description: 'Attachments found for the record', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity or record identifiers', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Upload attachment',\n description: 'Uploads a new attachment using multipart form-data and stores metadata for later retrieval.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: attachmentUploadBodySchema,\n },\n responses: [\n { status: 200, description: 'Attachment stored successfully', schema: uploadResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Payload validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Attachment violates field constraints', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Removes an uploaded attachment and deletes the stored asset.',\n query: attachmentDeleteQuerySchema,\n responses: [\n { status: 200, description: 'Attachment deleted', schema: z.object({ ok: z.literal(true) }) },\n { status: 404, description: 'Attachment not found', schema: errorSchema },\n ],\n errors: [\n { status: 400, description: 'Missing attachment identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { extractAttachmentContent } from '../lib/textExtraction'\nimport { requestOcrProcessing } from '../lib/ocrQueue'\nimport { StorageDriverFactory } from '../lib/drivers'\nimport { OcrService, shouldUseLlmOcr } from '../lib/ocrService'\nimport { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from '../lib/metadata'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from '../lib/security'\nimport {\n isMultipartRequestWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '../lib/upload-limits'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\nconst attachmentQuerySchema = z.object({\n entityId: z.string().min(1).describe('Entity identifier that owns the attachments'),\n recordId: z.string().min(1).describe('Record identifier within the entity'),\n page: z.coerce.number().min(1).optional(),\n pageSize: z.coerce.number().min(1).max(100).optional(),\n})\n\nconst attachmentAssignmentSchema = z.object({\n type: z.string().describe('Assignment type identifier'),\n id: z.string().describe('Assignment record identifier'),\n href: z.string().nullable().optional().describe('Optional link to the related record'),\n label: z.string().nullable().optional().describe('Optional label for the assignment'),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string().describe('Attachment identifier'),\n url: z.string().describe('Public path to the stored asset'),\n fileName: z.string().describe('Original filename'),\n fileSize: z.number().int().nonnegative().describe('File size in bytes'),\n createdAt: z.string().describe('Upload timestamp (ISO 8601)'),\n mimeType: z.string().nullable().optional().describe('MIME type of the file'),\n thumbnailUrl: z.string().optional().describe('Helper route that renders a thumbnail'),\n partitionCode: z.string().optional().describe('Partition identifier'),\n tags: z.array(z.string()).optional().describe('Tags assigned to the attachment'),\n content: z.string().nullable().optional().describe('Extracted text or markdown content'),\n assignments: z.array(attachmentAssignmentSchema).optional().describe('Records that reference this attachment'),\n})\n\nconst attachmentListResponseSchema = z.object({\n items: z.array(attachmentItemSchema),\n total: z.number().int().nonnegative().optional(),\n page: z.number().int().min(1).optional(),\n pageSize: z.number().int().min(1).optional(),\n totalPages: z.number().int().min(1).optional(),\n})\n\nconst attachmentUploadBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n fieldKey: z.string().optional(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n customFields: z\n .string()\n .optional()\n .describe('JSON encoded map of custom field values collected from the upload form.'),\n})\n\nconst attachmentDeleteQuerySchema = z.object({\n id: z.string().uuid(),\n})\n\nconst uploadResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string(),\n url: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n thumbnailUrl: z.string().optional(),\n content: z.string().nullable().optional(),\n tags: z.array(z.string()).optional(),\n assignments: z.array(attachmentAssignmentSchema).optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n }),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction parseCustomFieldsEntry(value: FormDataEntryValue | null): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return {}\n try {\n const parsed = JSON.parse(trimmed)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n } catch {\n return {}\n }\n }\n if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n return { ...(value as Record<string, unknown>) }\n }\n return {}\n}\n\nfunction buildFormPayload(form: FormData): Record<string, unknown> {\n const payload: Record<string, unknown> = {}\n form.forEach((value, key) => {\n if (key === 'customFields') {\n payload.customFields = parseCustomFieldsEntry(value)\n return\n }\n payload[key] = value\n })\n return payload\n}\n\nfunction parseFormTags(value: FormDataEntryValue | null): string[] {\n if (!value) return []\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentTags(parsed)\n } catch {\n return normalizeAttachmentTags(value)\n }\n }\n return []\n}\n\nfunction parseFormAssignments(value: FormDataEntryValue | null): AttachmentAssignment[] {\n if (!value) return []\n if (typeof value !== 'string') return []\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentAssignments(parsed)\n } catch {\n return []\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsedQuery = attachmentQuerySchema.safeParse({\n entityId: url.searchParams.get('entityId') || '',\n recordId: url.searchParams.get('recordId') || '',\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'entityId and recordId are required' }, { status: 400 })\n }\n const { entityId, recordId, page, pageSize } = parsedQuery.data\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const filter: Record<string, unknown> = { entityId, recordId, tenantId: auth.tenantId! }\n if (auth.orgId) filter.organizationId = auth.orgId\n const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC' }\n const usePaging = typeof page === 'number' && typeof pageSize === 'number'\n const total = usePaging ? await em.count(Attachment, filter) : null\n const currentPage = usePaging ? Math.max(1, page) : null\n const currentPageSize = usePaging ? pageSize : null\n const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null\n const pageOffset = usePaging ? (Math.min(currentPage!, totalPages!) - 1) * currentPageSize! : undefined\n const items = await findWithDecryption(\n em,\n Attachment,\n filter,\n {\n orderBy,\n ...(usePaging\n ? {\n limit: currentPageSize!,\n offset: pageOffset,\n }\n : {}),\n },\n {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n },\n )\n return NextResponse.json({\n items: items.map((a: any) => {\n const metadata = readAttachmentMetadata(a.storageMetadata)\n return {\n id: a.id,\n url: a.url,\n fileName: a.fileName,\n fileSize: a.fileSize,\n createdAt: a.createdAt,\n mimeType: a.mimeType ?? null,\n partitionCode: a.partitionCode,\n content: a.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(a.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(a.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n }\n }),\n ...(usePaging\n ? {\n total,\n page: Math.min(currentPage!, totalPages!),\n pageSize: currentPageSize,\n totalPages,\n }\n : {}),\n })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const tenantId = auth.tenantId\n const orgId = auth.orgId\n\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ error: 'Expected multipart/form-data' }, { status: 400 })\n }\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({ error: 'Attachment exceeds the maximum upload size.' }, { status: 413 })\n }\n\n const form = await req.formData()\n const formPayload = buildFormPayload(form)\n const customFieldValues = splitCustomFieldPayload(formPayload).custom\n const entityId = String(form.get('entityId') || '')\n const recordId = String(form.get('recordId') || '')\n const fieldKey = String(form.get('fieldKey') || '')\n const file = form.get('file') as unknown as File | null\n if (!entityId || !recordId || !file) return NextResponse.json({ error: 'entityId, recordId and file are required' }, { status: 400 })\n const partitionOverrideRaw = form.get('partitionCode')\n const partitionOverride =\n typeof partitionOverrideRaw === 'string' && partitionOverrideRaw.trim().length > 0\n ? sanitizePartitionCode(partitionOverrideRaw)\n : null\n const tags = parseFormTags(form.get('tags'))\n const assignmentsFromForm = parseFormAssignments(form.get('assignments'))\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine')\n const storageDriverFactory =\n (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n await ensureDefaultPartitions(em)\n // Optional per-field validations\n let partitionFromField: string | null = null\n let fieldMaxAttachmentSizeMb: number | null = null\n if (fieldKey) {\n try {\n const { CustomFieldDef } = await import('@open-mercato/core/modules/entities/data/entities')\n const def = await em.findOne(CustomFieldDef, {\n entityId,\n key: fieldKey,\n $and: [\n { $or: [ { tenantId: auth.tenantId }, { tenantId: null } ] },\n ],\n isActive: true,\n })\n const cfg = (def as any)?.configJson || {}\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n if (Array.isArray(cfg.acceptExtensions) && cfg.acceptExtensions.length) {\n const allowed = new Set((cfg.acceptExtensions as any[]).map((x: any) => String(x).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) return NextResponse.json({ error: 'File type not allowed' }, { status: 400 })\n }\n if (typeof cfg.maxAttachmentSizeMb === 'number' && cfg.maxAttachmentSizeMb > 0) {\n fieldMaxAttachmentSizeMb = cfg.maxAttachmentSizeMb\n }\n if (typeof cfg.partitionCode === 'string' && cfg.partitionCode.trim().length > 0) {\n partitionFromField = sanitizePartitionCode(cfg.partitionCode)\n }\n } catch {}\n }\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({\n error: t('attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'),\n }, { status: 400 })\n }\n const effectiveMaxBytes = resolveAttachmentMaxBytes(fieldMaxAttachmentSizeMb)\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n const buf = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeUploadedFileName(file.name)\n const fileMimeType = detectAttachmentMimeType(buf, safeName, (file as any).type)\n if (isActiveContentAttachment(buf, safeName, fileMimeType)) {\n return NextResponse.json({ error: t('attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n const defaultPartitionCode = resolveDefaultPartitionCode(entityId)\n const resolvedPartitionCode = partitionOverride ?? partitionFromField ?? defaultPartitionCode\n const partitionCodeCandidates = Array.from(\n new Set(\n [partitionOverride, partitionFromField, resolvedPartitionCode].filter(\n (code): code is string => typeof code === 'string' && code.length > 0,\n ),\n ),\n )\n let partition: AttachmentPartition | null = null\n for (const code of partitionCodeCandidates) {\n const record = await em.findOne(AttachmentPartition, { code })\n if (record) {\n partition = record\n break\n }\n }\n if (!partition) {\n partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode })\n }\n if (!partition) {\n return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })\n }\n const requestedPublicOverride =\n typeof partitionOverride === 'string' &&\n partitionOverride.length > 0 &&\n partition.code === partitionOverride &&\n partition.isPublic === true &&\n partition.code !== defaultPartitionCode &&\n partition.code !== partitionFromField\n if (requestedPublicOverride) {\n return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })\n }\n const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })\n let storedPath: string\n try {\n const stored = await uploadDriver.store({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n buffer: buf,\n })\n storedPath = stored.storagePath\n } catch (error) {\n console.error('[attachments] failed to persist file', error)\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n const requiresOcr =\n typeof (partition as any).requiresOcr === 'boolean'\n ? Boolean((partition as any).requiresOcr)\n : resolveDefaultAttachmentOcrEnabled()\n let extractedContent: string | null = null\n const wantsLlmOcr = requiresOcr && shouldUseLlmOcr(fileMimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n\n if (requiresOcr && !useLlmOcr) {\n const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({\n filePath: localPath,\n mimeType: fileMimeType,\n })\n } catch (error) {\n console.error('[attachments] failed to extract attachment content', error)\n } finally {\n await cleanup().catch(() => {})\n }\n }\n\n let assignments = assignmentsFromForm.slice()\n if (entityId !== LIBRARY_ENTITY_ID) {\n assignments = upsertAssignment(assignments, { type: entityId, id: recordId })\n }\n const metadata = mergeAttachmentMetadata(null, { assignments, tags })\n const attachmentId = randomUUID()\n const att = em.create(Attachment, {\n id: attachmentId,\n entityId,\n recordId,\n organizationId: auth.orgId!,\n tenantId: auth.tenantId!,\n fileName: safeName,\n mimeType: fileMimeType,\n fileSize: buf.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 await em.persist(att).flush()\n\n if (useLlmOcr) {\n requestOcrProcessing(em, att, uploadDriver, storedPath).catch((error) => {\n console.error('[attachments] failed to queue OCR processing', error)\n })\n } else if (wantsLlmOcr) {\n console.warn('[attachments] OCR requested but OPENAI_API_KEY not configured, falling back to text extraction when available')\n }\n\n if (dataEngine) {\n try {\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.attachments.attachment,\n recordId: attachmentId,\n tenantId,\n organizationId: orgId,\n values: customFieldValues,\n })\n } catch (error) {\n console.error('[attachments] failed to persist custom attributes', error)\n return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })\n }\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: att,\n identifiers: {\n id: att.id,\n organizationId: att.organizationId ?? null,\n tenantId: att.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({\n ok: true,\n item: {\n id: attachmentId,\n url: att.url,\n fileName: safeName,\n fileSize: buf.length,\n partitionCode: partition.code,\n thumbnailUrl: buildAttachmentImageUrl(attachmentId, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(safeName),\n }),\n content: extractedContent ?? null,\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n customFields: Object.keys(customFieldValues).length ? customFieldValues : undefined,\n },\n })\n}\n\nasync function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: string): Promise<number> {\n try {\n const db = em.getKysely<any>() as any\n const row = await db\n .selectFrom('attachments')\n .select(sql<string>`sum(file_size)`.as('total_size'))\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst() as { total_size: string | number | null } | undefined\n const total = row?.total_size\n if (typeof total === 'number') return Number.isFinite(total) ? total : 0\n if (typeof total === 'string') {\n const parsed = Number(total)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n } catch {\n return 0\n }\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const id = url.searchParams.get('id') || ''\n if (!id) return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine')\n const storageDriverFactory =\n (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId!, organizationId: auth.orgId }\n const record = await em.findOne(Attachment, deleteFilter)\n if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n await em.remove(record).flush()\n await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {\n console.error('[attachments] failed to cleanup cached thumbnails', error)\n })\n if (record.storagePath) {\n const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!,\n organizationId: record.organizationId ?? auth.orgId,\n })\n await delDriver.delete(record.partitionCode, record.storagePath)\n }\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? null,\n tenantId: record.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage entity attachments',\n description: 'Upload and list attachments associated with module entities and records.',\n methods: {\n GET: {\n summary: 'List attachments for a record',\n description: 'Returns uploaded attachments for the given entity record, ordered by newest first.',\n query: attachmentQuerySchema,\n responses: [\n { status: 200, description: 'Attachments found for the record', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity or record identifiers', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Upload attachment',\n description: 'Uploads a new attachment using multipart form-data and stores metadata for later retrieval.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: attachmentUploadBodySchema,\n },\n responses: [\n { status: 200, description: 'Attachment stored successfully', schema: uploadResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Payload validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Attachment violates field constraints', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Removes an uploaded attachment and deletes the stored asset.',\n query: attachmentDeleteQuerySchema,\n responses: [\n { status: 200, description: 'Attachment deleted', schema: z.object({ ok: z.literal(true) }) },\n { status: 404, description: 'Attachment not found', schema: errorSchema },\n ],\n errors: [\n { status: 400, description: 'Missing attachment identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,SAAS;AAClB,SAAS,WAAW;AAEpB,SAAS,wBAAwB,yBAAyB,iCAAiC;AAC3F,SAAS,yBAAyB,6BAA6B,6BAA6B;AAC5F,SAAS,YAAY,2BAA2B;AAChD,SAAS,gCAAgC;AACzC,SAAS,4BAA4B;AACrC,SAAS,4BAA4B;AACrC,SAAS,YAAY,uBAAuB;AAC5C,SAAS,qCAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,kBAAkB;AAE3B,SAAS,+BAA+B;AACxC,SAAS,qBAAqB,4BAA4B;AAC1D,SAAS,2BAA2B;AACpC,SAAS,sBAAsB,6BAA6B;AAC5D,SAAS,SAAS;AAClB,SAAS,0CAA0C;AACnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AAE5B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAAA,EAChE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AAAA,EACnE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AACvE;AAEA,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,6CAA6C;AAAA,EAClF,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,qCAAqC;AAAA,EAC1E,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AACvD,CAAC;AAED,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,MAAM,EAAE,OAAO,EAAE,SAAS,4BAA4B;AAAA,EACtD,IAAI,EAAE,OAAO,EAAE,SAAS,8BAA8B;AAAA,EACtD,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,EACrF,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,mCAAmC;AACtF,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,OAAO,EAAE,SAAS,uBAAuB;AAAA,EAC/C,KAAK,EAAE,OAAO,EAAE,SAAS,iCAAiC;AAAA,EAC1D,UAAU,EAAE,OAAO,EAAE,SAAS,mBAAmB;AAAA,EACjD,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,oBAAoB;AAAA,EACtE,WAAW,EAAE,OAAO,EAAE,SAAS,6BAA6B;AAAA,EAC5D,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,uBAAuB;AAAA,EAC3E,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,uCAAuC;AAAA,EACpF,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,sBAAsB;AAAA,EACpE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,SAAS,iCAAiC;AAAA,EAC/E,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,oCAAoC;AAAA,EACvF,aAAa,EAAE,MAAM,0BAA0B,EAAE,SAAS,EAAE,SAAS,wCAAwC;AAC/G,CAAC;AAED,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,OAAO,EAAE,MAAM,oBAAoB;AAAA,EACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;AAAA,EAC/C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACvC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC3C,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;AAC/C,CAAC;AAED,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,sDAAsD;AAAA,EACvF,cAAc,EACX,OAAO,EACP,SAAS,EACT,SAAS,yEAAyE;AACvF,CAAC;AAED,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAC3C,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,MAAM,EAAE,OAAO;AAAA,IACb,IAAI,EAAE,OAAO;AAAA,IACb,KAAK,EAAE,OAAO;AAAA,IACd,UAAU,EAAE,OAAO;AAAA,IACnB,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACvC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,IAClC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,IACnC,aAAa,EAAE,MAAM,0BAA0B,EAAE,SAAS;AAAA,IAC1D,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EAC3D,CAAC;AACH,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,oBAAoB;AAE1B,SAAS,uBAAuB,OAA2D;AACzF,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,OAAO;AACjC,UAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAClE,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,KAAK,EAAE,iBAAiB,OAAO;AAClF,WAAO,EAAE,GAAI,MAAkC;AAAA,EACjD;AACA,SAAO,CAAC;AACV;AAEA,SAAS,iBAAiB,MAAyC;AACjE,QAAM,UAAmC,CAAC;AAC1C,OAAK,QAAQ,CAAC,OAAO,QAAQ;AAC3B,QAAI,QAAQ,gBAAgB;AAC1B,cAAQ,eAAe,uBAAuB,KAAK;AACnD;AAAA,IACF;AACA,YAAQ,GAAG,IAAI;AAAA,EACjB,CAAC;AACD,SAAO;AACT;AAEA,SAAS,cAAc,OAA4C;AACjE,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,QAAS,QAAO,CAAC;AACtB,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,OAAO;AACjC,aAAO,wBAAwB,MAAM;AAAA,IACvC,QAAQ;AACN,aAAO,wBAAwB,KAAK;AAAA,IACtC;AAAA,EACF;AACA,SAAO,CAAC;AACV;AAEA,SAAS,qBAAqB,OAA0D;AACtF,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,MAAI,OAAO,UAAU,SAAU,QAAO,CAAC;AACvC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO,+BAA+B,MAAM;AAAA,EAC9C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAa,CAAC,KAAK,SAAS,CAAC,KAAK,aAAe,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AACvI,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,sBAAsB,UAAU;AAAA,IAClD,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,IAC9C,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,aAAa,KAAK,EAAE,OAAO,qCAAqC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AACA,QAAM,EAAE,UAAU,UAAU,MAAM,SAAS,IAAI,YAAY;AAE3D,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,SAAkC,EAAE,UAAU,UAAU,UAAU,KAAK,SAAU;AACvF,MAAI,KAAK,MAAO,QAAO,iBAAiB,KAAK;AAC7C,QAAM,UAA0C,EAAE,WAAW,OAAO;AACpE,QAAM,YAAY,OAAO,SAAS,YAAY,OAAO,aAAa;AAClE,QAAM,QAAQ,YAAY,MAAM,GAAG,MAAM,YAAY,MAAM,IAAI;AAC/D,QAAM,cAAc,YAAY,KAAK,IAAI,GAAG,IAAI,IAAI;AACpD,QAAM,kBAAkB,YAAY,WAAW;AAC/C,QAAM,aAAa,aAAa,UAAU,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,eAAgB,CAAC,IAAI;AACpG,QAAM,aAAa,aAAa,KAAK,IAAI,aAAc,UAAW,IAAI,KAAK,kBAAmB;AAC9F,QAAM,QAAQ,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,MACA,GAAI,YACA;AAAA,QACE,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,IACA,CAAC;AAAA,IACP;AAAA,IACA;AAAA,MACE,UAAU,KAAK,YAAY;AAAA,MAC3B,gBAAgB,KAAK,SAAS;AAAA,IAChC;AAAA,EACF;AACA,SAAO,aAAa,KAAK;AAAA,IACvB,OAAO,MAAM,IAAI,CAAC,MAAW;AAC3B,YAAMA,YAAW,uBAAuB,EAAE,eAAe;AACzD,aAAO;AAAA,QACL,IAAI,EAAE;AAAA,QACN,KAAK,EAAE;AAAA,QACP,UAAU,EAAE;AAAA,QACZ,UAAU,EAAE;AAAA,QACZ,WAAW,EAAE;AAAA,QACb,UAAU,EAAE,YAAY;AAAA,QACxB,eAAe,EAAE;AAAA,QACjB,SAAS,EAAE,WAAW;AAAA,QACtB,cAAc,wBAAwB,EAAE,IAAI;AAAA,UAC1C,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,MAAM,0BAA0B,EAAE,QAAQ;AAAA,QAC5C,CAAC;AAAA,QACD,MAAMA,UAAS,QAAQ,CAAC;AAAA,QACxB,aAAaA,UAAS,eAAe,CAAC;AAAA,MACxC;AAAA,IACF,CAAC;AAAA,IACD,GAAI,YACA;AAAA,MACE;AAAA,MACA,MAAM,KAAK,IAAI,aAAc,UAAW;AAAA,MACxC,UAAU;AAAA,MACV;AAAA,IACF,IACA,CAAC;AAAA,EACP,CAAC;AACH;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,KAAK,MAAO,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC/G,QAAM,WAAW,KAAK;AACtB,QAAM,QAAQ,KAAK;AAEnB,QAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,KAAK;AACvD,MAAI,CAAC,YAAY,YAAY,EAAE,SAAS,qBAAqB,GAAG;AAC9D,WAAO,aAAa,KAAK,EAAE,OAAO,+BAA+B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrF;AACA,MAAI,CAAC,oCAAoC,IAAI,QAAQ,IAAI,gBAAgB,CAAC,GAAG;AAC3E,WAAO,aAAa,KAAK,EAAE,OAAO,8CAA8C,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpG;AAEA,QAAM,OAAO,MAAM,IAAI,SAAS;AAChC,QAAM,cAAc,iBAAiB,IAAI;AACzC,QAAM,oBAAoB,wBAAwB,WAAW,EAAE;AAC/D,QAAM,WAAW,OAAO,KAAK,IAAI,UAAU,KAAK,EAAE;AAClD,QAAM,WAAW,OAAO,KAAK,IAAI,UAAU,KAAK,EAAE;AAClD,QAAM,WAAW,OAAO,KAAK,IAAI,UAAU,KAAK,EAAE;AAClD,QAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,MAAI,CAAC,YAAY,CAAC,YAAY,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,2CAA2C,GAAG,EAAE,QAAQ,IAAI,CAAC;AACpI,QAAM,uBAAuB,KAAK,IAAI,eAAe;AACrD,QAAM,oBACJ,OAAO,yBAAyB,YAAY,qBAAqB,KAAK,EAAE,SAAS,IAC7E,sBAAsB,oBAAoB,IAC1C;AACN,QAAM,OAAO,cAAc,KAAK,IAAI,MAAM,CAAC;AAC3C,QAAM,sBAAsB,qBAAqB,KAAK,IAAI,aAAa,CAAC;AAExE,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,uBACH,QAAQ,sBAAsB,KAAqC,IAAI,qBAAqB,EAAE;AACjG,QAAM,wBAAwB,EAAE;AAEhC,MAAI,qBAAoC;AACxC,MAAI,2BAA0C;AAC9C,MAAI,UAAU;AACZ,QAAI;AACF,YAAM,EAAE,eAAe,IAAI,MAAM,OAAO,mDAAmD;AAC3F,YAAM,MAAM,MAAM,GAAG,QAAQ,gBAAgB;AAAA,QAC3C;AAAA,QACA,KAAK;AAAA,QACL,MAAM;AAAA,UACJ,EAAE,KAAK,CAAE,EAAE,UAAU,KAAK,SAAS,GAAG,EAAE,UAAU,KAAK,CAAE,EAAE;AAAA,QAC7D;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,YAAM,MAAO,KAAa,cAAc,CAAC;AACzC,YAAM,OAAO,KAAK,QAAQ,IAAI,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY,KAAK;AACjE,UAAI,MAAM,QAAQ,IAAI,gBAAgB,KAAK,IAAI,iBAAiB,QAAQ;AACtE,cAAM,UAAU,IAAI,IAAK,IAAI,iBAA2B,IAAI,CAAC,MAAW,OAAO,CAAC,EAAE,YAAY,EAAE,QAAQ,OAAO,EAAE,CAAC,CAAC;AACnH,YAAI,CAAC,QAAQ,IAAI,GAAG,EAAG,QAAO,aAAa,KAAK,EAAE,OAAO,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,MACrG;AACA,UAAI,OAAO,IAAI,wBAAwB,YAAY,IAAI,sBAAsB,GAAG;AAC9E,mCAA2B,IAAI;AAAA,MACjC;AACA,UAAI,OAAO,IAAI,kBAAkB,YAAY,IAAI,cAAc,KAAK,EAAE,SAAS,GAAG;AAChF,6BAAqB,sBAAsB,IAAI,aAAa;AAAA,MAC9D;AAAA,IACF,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,MAAI,gCAAgC,KAAK,IAAI,GAAG;AAC9C,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,EAAE,0CAA0C,uDAAuD;AAAA,IAC5G,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpB;AACA,QAAM,oBAAoB,0BAA0B,wBAAwB;AAC5E,MAAI,KAAK,OAAO,mBAAmB;AACjC,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,EAAE,oCAAoC,6CAA6C;AAAA,IAC5F,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpB;AACA,QAAM,mBAAmB,MAAM,+BAA+B,IAAI,QAAQ;AAC1E,MAAI,gCAAgC,kBAAkB,KAAK,IAAI,GAAG;AAChE,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,EAAE,oCAAoC,oDAAoD;AAAA,IACnG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpB;AACA,QAAM,MAAM,OAAO,KAAK,MAAM,KAAK,YAAY,CAAC;AAChD,QAAM,WAAW,yBAAyB,KAAK,IAAI;AACnD,QAAM,eAAe,yBAAyB,KAAK,UAAW,KAAa,IAAI;AAC/E,MAAI,0BAA0B,KAAK,UAAU,YAAY,GAAG;AAC1D,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,2CAA2C,yCAAyC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9I;AACA,QAAM,uBAAuB,4BAA4B,QAAQ;AACjE,QAAM,wBAAwB,qBAAqB,sBAAsB;AACzE,QAAM,0BAA0B,MAAM;AAAA,IACpC,IAAI;AAAA,MACF,CAAC,mBAAmB,oBAAoB,qBAAqB,EAAE;AAAA,QAC7D,CAAC,SAAyB,OAAO,SAAS,YAAY,KAAK,SAAS;AAAA,MACtE;AAAA,IACF;AAAA,EACF;AACA,MAAI,YAAwC;AAC5C,aAAW,QAAQ,yBAAyB;AAC1C,UAAM,SAAS,MAAM,GAAG,QAAQ,qBAAqB,EAAE,KAAK,CAAC;AAC7D,QAAI,QAAQ;AACV,kBAAY;AACZ;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,WAAW;AACd,gBAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAAA,EAClF;AACA,MAAI,CAAC,WAAW;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,uCAAuC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7F;AACA,QAAM,0BACJ,OAAO,sBAAsB,YAC7B,kBAAkB,SAAS,KAC3B,UAAU,SAAS,qBACnB,UAAU,aAAa,QACvB,UAAU,SAAS,wBACnB,UAAU,SAAS;AACrB,MAAI,yBAAyB;AAC3B,WAAO,aAAa,KAAK,EAAE,OAAO,EAAE,6CAA6C,0EAA0E,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjL;AACA,QAAM,eAAe,MAAM,qBAAqB,oBAAoB,UAAU,MAAM,EAAE,UAAU,gBAAgB,MAAM,CAAC;AACvH,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,aAAa,MAAM;AAAA,MACtC,eAAe,UAAU;AAAA,MACzB;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,QAAQ;AAAA,IACV,CAAC;AACD,iBAAa,OAAO;AAAA,EACtB,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC,KAAK;AAC3D,WAAO,aAAa,KAAK,EAAE,OAAO,gCAAgC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACtF;AAEA,QAAM,cACJ,OAAQ,UAAkB,gBAAgB,YACtC,QAAS,UAAkB,WAAW,IACtC,mCAAmC;AACzC,MAAI,mBAAkC;AACtC,QAAM,cAAc,eAAe,gBAAgB,cAAc,QAAQ;AACzE,QAAM,aAAa,cAAc,IAAI,WAAW,IAAI;AACpD,QAAM,YAAY,QAAQ,eAAe,YAAY,SAAS;AAE9D,MAAI,eAAe,CAAC,WAAW;AAC7B,UAAM,EAAE,UAAU,WAAW,QAAQ,IAAI,MAAM,aAAa,YAAY,UAAU,MAAM,UAAU;AAClG,QAAI;AACF,yBAAmB,MAAM,yBAAyB;AAAA,QAChD,UAAU;AAAA,QACV,UAAU;AAAA,MACZ,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,MAAM,sDAAsD,KAAK;AAAA,IAC3E,UAAE;AACA,YAAM,QAAQ,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAChC;AAAA,EACF;AAEA,MAAI,cAAc,oBAAoB,MAAM;AAC5C,MAAI,aAAa,mBAAmB;AAClC,kBAAc,iBAAiB,aAAa,EAAE,MAAM,UAAU,IAAI,SAAS,CAAC;AAAA,EAC9E;AACA,QAAMA,YAAW,wBAAwB,MAAM,EAAE,aAAa,KAAK,CAAC;AACpE,QAAM,eAAe,WAAW;AAChC,QAAM,MAAM,GAAG,OAAO,YAAY;AAAA,IAChC,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA,gBAAgB,KAAK;AAAA,IACrB,UAAU,KAAK;AAAA,IACf,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU,IAAI;AAAA,IACd,eAAe,UAAU;AAAA,IACzB,eAAe,UAAU,iBAAiB;AAAA,IAC1C,aAAa;AAAA,IACb,KAAK,uBAAuB,YAAY;AAAA,IACxC,SAAS;AAAA,IACT,iBAAiBA;AAAA,EACnB,CAAC;AACD,QAAM,GAAG,QAAQ,GAAG,EAAE,MAAM;AAE5B,MAAI,WAAW;AACb,yBAAqB,IAAI,KAAK,cAAc,UAAU,EAAE,MAAM,CAAC,UAAU;AACvE,cAAQ,MAAM,gDAAgD,KAAK;AAAA,IACrE,CAAC;AAAA,EACH,WAAW,aAAa;AACtB,YAAQ,KAAK,+GAA+G;AAAA,EAC9H;AAEA,MAAI,YAAY;AACd,QAAI;AACF,YAAM,qBAAqB;AAAA,QACzB;AAAA,QACA,UAAU,EAAE,YAAY;AAAA,QACxB,UAAU;AAAA,QACV;AAAA,QACA,gBAAgB;AAAA,QAChB,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,MAAM,qDAAqD,KAAK;AACxE,aAAO,aAAa,KAAK,EAAE,OAAO,wCAAwC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9F;AACA,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI,kBAAkB;AAAA,QACtC,UAAU,IAAI,YAAY;AAAA,MAC5B;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,WAAW,sBAAsB;AAAA,EACzC;AAEA,SAAO,aAAa,KAAK;AAAA,IACvB,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,IAAI;AAAA,MACJ,KAAK,IAAI;AAAA,MACT,UAAU;AAAA,MACV,UAAU,IAAI;AAAA,MACd,eAAe,UAAU;AAAA,MACzB,cAAc,wBAAwB,cAAc;AAAA,QAClD,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,MAAM,0BAA0B,QAAQ;AAAA,MAC1C,CAAC;AAAA,MACD,SAAS,oBAAoB;AAAA,MAC7B,MAAMA,UAAS,QAAQ,CAAC;AAAA,MACxB,aAAaA,UAAS,eAAe,CAAC;AAAA,MACtC,cAAc,OAAO,KAAK,iBAAiB,EAAE,SAAS,oBAAoB;AAAA,IAC5E;AAAA,EACF,CAAC;AACH;AAEA,eAAe,+BAA+B,IAAmB,UAAmC;AAClG,MAAI;AACF,UAAM,KAAK,GAAG,UAAe;AAC7B,UAAM,MAAM,MAAM,GACf,WAAW,aAAa,EACxB,OAAO,oBAA4B,GAAG,YAAY,CAAC,EACnD,MAAM,aAAa,KAAK,QAAQ,EAChC,iBAAiB;AACpB,UAAM,QAAQ,KAAK;AACnB,QAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AACvE,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,SAAS,OAAO,KAAK;AAC3B,aAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAAA,IAC5C;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,OAAO,KAAc;AACzC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC,KAAK,MAAO,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC/G,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,KAAK,IAAI,aAAa,IAAI,IAAI,KAAK;AACzC,MAAI,CAAC,GAAI,QAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AACzF,QAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,QAAM,KAAK,QAAQ,IAAI;AACvB,QAAM,aAAa,QAAQ,YAAY;AACvC,QAAM,uBACH,QAAQ,sBAAsB,KAAqC,IAAI,qBAAqB,EAAE;AACjG,QAAM,eAAwC,EAAE,IAAI,UAAU,KAAK,UAAW,gBAAgB,KAAK,MAAM;AACzG,QAAM,SAAS,MAAM,GAAG,QAAQ,YAAY,YAAY;AACxD,MAAI,CAAC,OAAQ,QAAO,aAAa,KAAK,EAAE,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AACxF,QAAM,GAAG,OAAO,MAAM,EAAE,MAAM;AAC9B,QAAM,8BAA8B,OAAO,eAAe,OAAO,EAAE,EAAE,MAAM,CAAC,UAAU;AACpF,YAAQ,MAAM,qDAAqD,KAAK;AAAA,EAC1E,CAAC;AACD,MAAI,OAAO,aAAa;AACtB,UAAM,YAAY,MAAM,qBAAqB,oBAAoB,OAAO,eAAe;AAAA,MACrF,UAAU,OAAO,YAAY,KAAK;AAAA,MAClC,gBAAgB,OAAO,kBAAkB,KAAK;AAAA,IAChD,CAAC;AACD,UAAM,UAAU,OAAO,OAAO,eAAe,OAAO,WAAW;AAAA,EACjE;AACA,MAAI,YAAY;AACd,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO,kBAAkB;AAAA,QACzC,UAAU,OAAO,YAAY;AAAA,MAC/B;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,WAAW,sBAAsB;AAAA,EACzC;AACA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,oCAAoC,QAAQ,6BAA6B;AAAA,MACvG;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wCAAwC,QAAQ,YAAY;AAAA,QACxF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,MAClE;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,kCAAkC,QAAQ,qBAAqB;AAAA,MAC7F;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,4BAA4B,QAAQ,YAAY;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,yCAAyC,QAAQ,YAAY;AAAA,MAC3F;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,MACb,OAAO;AAAA,MACP,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,QAC5F,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,YAAY;AAAA,MAC1E;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,YAAY;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,YAAY;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["metadata"]
|
|
7
7
|
}
|
|
@@ -2,10 +2,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
3
3
|
import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
|
|
4
4
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
5
6
|
import { AttachmentPartitionSettings } from "../../../components/AttachmentPartitionSettings.js";
|
|
6
7
|
import { isPartitionSettingsLocked } from "../../../lib/partitions.js";
|
|
7
8
|
async function AttachmentsConfigurationPage() {
|
|
8
9
|
const partitionsLocked = isPartitionSettingsLocked();
|
|
10
|
+
const s3Enabled = parseBooleanWithDefault(process.env.OM_ENABLE_STORAGE_S3, false);
|
|
9
11
|
const { t } = await resolveTranslations();
|
|
10
12
|
const lockedTitle = t("attachments.partitions.locked.title", "Partition settings locked");
|
|
11
13
|
const lockedDescription = t(
|
|
@@ -30,7 +32,7 @@ async function AttachmentsConfigurationPage() {
|
|
|
30
32
|
"."
|
|
31
33
|
] })
|
|
32
34
|
] }) : null,
|
|
33
|
-
!partitionsLocked ? /* @__PURE__ */ jsx(AttachmentPartitionSettings, {}) : null
|
|
35
|
+
!partitionsLocked ? /* @__PURE__ */ jsx(AttachmentPartitionSettings, { s3Enabled }) : null
|
|
34
36
|
] }) });
|
|
35
37
|
}
|
|
36
38
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/attachments/backend/config/attachments/page.tsx"],
|
|
4
|
-
"sourcesContent": ["import { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { AttachmentPartitionSettings } from '../../../components/AttachmentPartitionSettings'\nimport { isPartitionSettingsLocked } from '../../../lib/partitions'\n\nexport default async function AttachmentsConfigurationPage() {\n const partitionsLocked = isPartitionSettingsLocked()\n const { t } = await resolveTranslations()\n const lockedTitle = t('attachments.partitions.locked.title', 'Partition settings locked')\n const lockedDescription = t(\n 'attachments.partitions.locked.description',\n 'Attachment partitions are managed by environment defaults while demo or onboarding mode is enabled.',\n )\n const lockedHint = t(\n 'attachments.partitions.locked.hint',\n 'Update the environment variables below to re-enable in-app configuration.',\n )\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n {partitionsLocked ? (\n <Alert variant=\"warning\">\n <AlertTitle>{lockedTitle}</AlertTitle>\n <AlertDescription>\n {lockedDescription}{' '}\n {lockedHint}{' '}\n <code>DEMO_MODE</code> / <code>SELF_SERVICE_ONBOARDING_ENABLED</code>.\n </AlertDescription>\n </Alert>\n ) : null}\n {!partitionsLocked ? <AttachmentPartitionSettings /> : null}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { AttachmentPartitionSettings } from '../../../components/AttachmentPartitionSettings'\nimport { isPartitionSettingsLocked } from '../../../lib/partitions'\n\nexport default async function AttachmentsConfigurationPage() {\n const partitionsLocked = isPartitionSettingsLocked()\n const s3Enabled = parseBooleanWithDefault(process.env.OM_ENABLE_STORAGE_S3, false)\n const { t } = await resolveTranslations()\n const lockedTitle = t('attachments.partitions.locked.title', 'Partition settings locked')\n const lockedDescription = t(\n 'attachments.partitions.locked.description',\n 'Attachment partitions are managed by environment defaults while demo or onboarding mode is enabled.',\n )\n const lockedHint = t(\n 'attachments.partitions.locked.hint',\n 'Update the environment variables below to re-enable in-app configuration.',\n )\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n {partitionsLocked ? (\n <Alert variant=\"warning\">\n <AlertTitle>{lockedTitle}</AlertTitle>\n <AlertDescription>\n {lockedDescription}{' '}\n {lockedHint}{' '}\n <code>DEMO_MODE</code> / <code>SELF_SERVICE_ONBOARDING_ENABLED</code>.\n </AlertDescription>\n </Alert>\n ) : null}\n {!partitionsLocked ? <AttachmentPartitionSettings s3Enabled={s3Enabled} /> : null}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "AA0BY,cACA,YADA;AA1BZ,SAAS,MAAM,gBAAgB;AAC/B,SAAS,OAAO,kBAAkB,kBAAkB;AACpD,SAAS,2BAA2B;AACpC,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAC5C,SAAS,iCAAiC;AAE1C,eAAO,+BAAsD;AAC3D,QAAM,mBAAmB,0BAA0B;AACnD,QAAM,YAAY,wBAAwB,QAAQ,IAAI,sBAAsB,KAAK;AACjF,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,cAAc,EAAE,uCAAuC,2BAA2B;AACxF,QAAM,oBAAoB;AAAA,IACxB;AAAA,IACA;AAAA,EACF;AACA,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AAEA,SACE,oBAAC,QACC,+BAAC,YAAS,WAAU,aACjB;AAAA,uBACC,qBAAC,SAAM,SAAQ,WACb;AAAA,0BAAC,cAAY,uBAAY;AAAA,MACzB,qBAAC,oBACE;AAAA;AAAA,QAAmB;AAAA,QACnB;AAAA,QAAY;AAAA,QACb,oBAAC,UAAK,uBAAS;AAAA,QAAO;AAAA,QAAG,oBAAC,UAAK,6CAA+B;AAAA,QAAO;AAAA,SACvE;AAAA,OACF,IACE;AAAA,IACH,CAAC,mBAAmB,oBAAC,+BAA4B,WAAsB,IAAK;AAAA,KAC/E,GACF;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
2
|
import { Attachment } from "./data/entities.js";
|
|
3
|
-
import { deletePartitionFile } from "./lib/storage.js";
|
|
4
3
|
function parseArgs(rest) {
|
|
5
4
|
const args = {};
|
|
6
5
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -43,6 +42,7 @@ const deleteAttachments = {
|
|
|
43
42
|
try {
|
|
44
43
|
const container = await createRequestContainer();
|
|
45
44
|
const em = container.resolve("em");
|
|
45
|
+
const storageDriverFactory = container.resolve("storageDriverFactory");
|
|
46
46
|
const idList = Array.from(ids);
|
|
47
47
|
const where = { id: { $in: idList } };
|
|
48
48
|
if (organizationId) where.organizationId = organizationId;
|
|
@@ -54,7 +54,11 @@ const deleteAttachments = {
|
|
|
54
54
|
}
|
|
55
55
|
const removedIds = /* @__PURE__ */ new Set();
|
|
56
56
|
for (const entry of attachments) {
|
|
57
|
-
await
|
|
57
|
+
const driver = await storageDriverFactory.resolveForPartition(entry.partitionCode, {
|
|
58
|
+
tenantId: entry.tenantId ?? "",
|
|
59
|
+
organizationId: entry.organizationId ?? ""
|
|
60
|
+
});
|
|
61
|
+
await driver.delete(entry.partitionCode, entry.storagePath);
|
|
58
62
|
em.remove(entry);
|
|
59
63
|
removedIds.add(entry.id);
|
|
60
64
|
console.log(`Deleted attachment ${entry.id}${entry.fileName ? ` (${entry.fileName})` : ""}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/attachments/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment } from './data/entities'\nimport {
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AAEvC,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment } from './data/entities'\nimport type { StorageDriverFactory } from './lib/drivers'\n\ntype ParsedArgs = Record<string, string | boolean>\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part || !part.startsWith('--')) continue\n const [rawKey, rawValue] = part.replace(/^--/, '').split('=')\n const key = rawKey.trim()\n if (!key) continue\n if (rawValue !== undefined) {\n args[key] = rawValue\n continue\n }\n const next = rest[i + 1]\n if (next && !next.startsWith('--')) {\n args[key] = next\n i += 1\n } else {\n args[key] = true\n }\n }\n return args\n}\n\nfunction coerceIdList(value?: string | boolean): string[] {\n if (typeof value !== 'string') return []\n return value\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n}\n\nconst deleteAttachments: ModuleCli = {\n command: 'delete',\n async run(rest) {\n const args = parseArgs(rest)\n const ids = new Set<string>()\n coerceIdList(args.id).forEach((id) => ids.add(id))\n coerceIdList(args.ids).forEach((id) => ids.add(id))\n\n if (ids.size === 0) {\n console.error('Usage: mercato attachments delete --id <attachmentId> [--ids id1,id2] [--org <organizationId>] [--tenant <tenantId>]')\n return\n }\n const organizationId =\n typeof args.org === 'string'\n ? args.org\n : typeof args.organizationId === 'string'\n ? args.organizationId\n : undefined\n const tenantId =\n typeof args.tenant === 'string'\n ? args.tenant\n : typeof args.tenantId === 'string'\n ? args.tenantId\n : undefined\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const storageDriverFactory = container.resolve<StorageDriverFactory>('storageDriverFactory')\n const idList = Array.from(ids)\n const where: Record<string, unknown> = { id: { $in: idList } }\n if (organizationId) where.organizationId = organizationId\n if (tenantId) where.tenantId = tenantId\n const attachments = await em.find(Attachment, where)\n if (!attachments.length) {\n console.log('No attachments matched the provided filters.')\n return\n }\n const removedIds = new Set<string>()\n for (const entry of attachments) {\n const driver = await storageDriverFactory.resolveForPartition(entry.partitionCode, {\n tenantId: entry.tenantId ?? '',\n organizationId: entry.organizationId ?? '',\n })\n await driver.delete(entry.partitionCode, entry.storagePath)\n em.remove(entry)\n removedIds.add(entry.id)\n console.log(`Deleted attachment ${entry.id}${entry.fileName ? ` (${entry.fileName})` : ''}`)\n }\n await em.flush()\n const missing = idList.filter((id) => !removedIds.has(id))\n if (missing.length > 0) {\n console.log(`Not found: ${missing.join(', ')}`)\n }\n } catch (err) {\n console.error('[attachments] delete command failed:', err)\n }\n },\n}\n\nexport default [deleteAttachments]\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|