@open-mercato/core 0.6.2-develop.3381.1.8461b312a9 → 0.6.2-develop.3400.2.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/integrations/backend/integrations/bundle/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n setCredValues(credCall.result.credentials)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { CredentialFieldType, IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'\nimport { LoadingMessage } from '@open-mercato/ui/backend/detail'\nimport { ErrorMessage } from '@open-mercato/ui/backend/detail'\n\ntype CredentialField = IntegrationCredentialField\n\nconst UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])\n\nfunction isEditableCredentialField(field: CredentialField): boolean {\n return !UNSUPPORTED_CREDENTIAL_FIELD_TYPES.has(field.type)\n}\n\ntype BundleIntegration = {\n id: string\n title: string\n description?: string\n category?: string\n isEnabled: boolean\n}\n\ntype BundleDetail = {\n integration: {\n id: string\n title: string\n description?: string\n bundleId?: string\n }\n bundle?: {\n id: string\n title: string\n description?: string\n credentials?: { fields: CredentialField[] }\n }\n bundleIntegrations: BundleIntegration[]\n state: { isEnabled: boolean }\n hasCredentials: boolean\n}\n\ntype BundleConfigPageProps = {\n params?: {\n id?: string | string[]\n }\n}\n\nfunction resolveRouteId(value: string | string[] | undefined): string | undefined {\n if (Array.isArray(value)) return value[0]\n return value\n}\n\nfunction resolvePathnameId(pathname: string): string | undefined {\n const parts = pathname.split('/').filter(Boolean)\n const bundleId = parts.at(-1)\n if (!bundleId || bundleId === 'bundle' || bundleId === 'integrations') return undefined\n return decodeURIComponent(bundleId)\n}\n\nexport default function BundleConfigPage({ params }: BundleConfigPageProps) {\n const pathname = usePathname()\n const bundleId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)\n const t = useT()\n\n const [detail, setDetail] = React.useState<BundleDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})\n const [isSavingCreds, setIsSavingCreds] = React.useState(false)\n const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())\n\n const resolveCurrentBundleId = React.useCallback(() => {\n return bundleId ?? (\n typeof window !== 'undefined'\n ? resolvePathnameId(window.location.pathname)\n : undefined\n )\n }, [bundleId])\n\n const load = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setIsLoading(true)\n setError(null)\n const call = await apiCall<BundleDetail>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}`,\n undefined,\n { fallback: null },\n )\n if (!call.ok || !call.result) {\n setError(t('integrations.detail.loadError'))\n setIsLoading(false)\n return\n }\n setDetail(call.result)\n\n const credCall = await apiCall<{ credentials: Record<string, unknown> }>(\n `/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`,\n undefined,\n { fallback: null },\n )\n if (credCall.ok && credCall.result?.credentials) {\n const next = { ...credCall.result.credentials }\n if (currentBundleId === 'storage_s3') {\n const authMode = next.authMode\n if (authMode !== 'access_keys' && authMode !== 'ambient') {\n const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)\n next.authMode = hasKeys ? 'access_keys' : 'ambient'\n }\n }\n setCredValues(next)\n }\n setIsLoading(false)\n }, [resolveCurrentBundleId, t])\n\n React.useEffect(() => { void load() }, [load])\n\n const handleSaveCredentials = React.useCallback(async () => {\n const currentBundleId = resolveCurrentBundleId()\n if (!currentBundleId) return\n setIsSavingCreds(true)\n const call = await apiCall(`/api/integrations/${encodeURIComponent(currentBundleId)}/credentials`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: credValues }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('integrations.detail.credentials.saved'), 'success')\n } else {\n flash(t('integrations.detail.credentials.saveError'), 'error')\n }\n setIsSavingCreds(false)\n }, [resolveCurrentBundleId, credValues, t])\n\n const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {\n setTogglingIds((prev) => new Set(prev).add(integrationId))\n const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ isEnabled: enabled }),\n }, { fallback: null })\n if (call.ok) {\n setDetail((prev) => {\n if (!prev) return prev\n return {\n ...prev,\n bundleIntegrations: prev.bundleIntegrations.map((item) =>\n item.id === integrationId ? { ...item, isEnabled: enabled } : item,\n ),\n }\n })\n } else {\n flash(t('integrations.detail.stateError'), 'error')\n }\n setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })\n }, [t])\n\n const handleBulkToggle = React.useCallback(async (enabled: boolean) => {\n if (!detail) return\n const targets = detail.bundleIntegrations.filter((item) => item.isEnabled !== enabled)\n await Promise.all(targets.map((item) => handleToggle(item.id, enabled)))\n }, [detail, handleToggle])\n\n if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.bundle.title')} /></PageBody></Page>\n if (error || !detail?.bundle) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>\n\n const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)\n\n function isFieldVisible(field: CredentialField): boolean {\n if (!field.visibleWhen) return true\n return credValues[field.visibleWhen.field] === field.visibleWhen.equals\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <div>\n <Link href=\"/backend/integrations\" className=\"text-sm text-muted-foreground hover:underline\">\n {t('integrations.detail.back')}\n </Link>\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold\">{detail.bundle.title}</h1>\n {detail.bundle.description && (\n <p className=\"text-muted-foreground mt-1\">{detail.bundle.description}</p>\n )}\n </div>\n\n {credFields.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {credFields.filter(isFieldVisible).map((field) => (\n <div key={field.key} className=\"space-y-1.5\">\n <label className=\"text-sm font-medium\">\n {field.label}{field.required && <span className=\"text-red-500 ml-0.5\">*</span>}\n </label>\n {field.type === 'select' && field.options ? (\n <Select\n value={(credValues[field.key] as string) || undefined}\n onValueChange={(value) => setCredValues((prev) => ({ ...prev, [field.key]: value ?? '' }))}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"\u2014\" />\n </SelectTrigger>\n <SelectContent>\n {field.options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : field.type === 'boolean' ? (\n <Switch\n checked={Boolean(credValues[field.key])}\n onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}\n />\n ) : (\n <Input\n type={field.type === 'secret' ? 'password' : 'text'}\n placeholder={field.placeholder}\n value={(credValues[field.key] as string) ?? ''}\n onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}\n />\n )}\n </div>\n ))}\n <Button type=\"button\" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>\n {isSavingCreds ? <Spinner className=\"mr-2 h-4 w-4\" /> : null}\n {t('integrations.detail.credentials.save')}\n </Button>\n </CardContent>\n </Card>\n )}\n\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle>{t('integrations.bundle.integrationToggles')}</CardTitle>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(true)}>\n {t('integrations.marketplace.enableAll')}\n </Button>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleBulkToggle(false)}>\n {t('integrations.marketplace.disableAll')}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-3\">\n {detail.bundleIntegrations.map((item) => (\n <div key={item.id} className=\"flex items-center justify-between rounded-lg border p-3\">\n <div>\n <Link\n href={`/backend/integrations/${encodeURIComponent(item.id)}`}\n className=\"text-sm font-medium hover:underline\"\n >\n {item.title}\n </Link>\n {item.category && (\n <Badge variant=\"secondary\" className=\"ml-2 text-xs\">{item.category}</Badge>\n )}\n {item.description && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{item.description}</p>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n <Button asChild variant=\"ghost\" size=\"sm\">\n <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>\n {t('integrations.bundle.configureIntegration')}\n </Link>\n </Button>\n <Switch\n checked={item.isEnabled}\n disabled={togglingIds.has(item.id)}\n onCheckedChange={(checked) => void handleToggle(item.id, checked)}\n />\n </div>\n </div>\n ))}\n </div>\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.2-develop.
|
|
3
|
+
"version": "0.6.2-develop.3400.2.d948268f50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -109,6 +109,10 @@
|
|
|
109
109
|
"types": "./src/modules/attachments/index.ts",
|
|
110
110
|
"default": "./dist/modules/attachments/index.js"
|
|
111
111
|
},
|
|
112
|
+
"./modules/attachments/lib/drivers": {
|
|
113
|
+
"types": "./src/modules/attachments/lib/drivers/index.ts",
|
|
114
|
+
"default": "./dist/modules/attachments/lib/drivers/index.js"
|
|
115
|
+
},
|
|
112
116
|
"./modules/dashboards": {
|
|
113
117
|
"types": "./src/modules/dashboards/index.ts",
|
|
114
118
|
"default": "./dist/modules/dashboards/index.js"
|
|
@@ -238,16 +242,16 @@
|
|
|
238
242
|
"zod": "^4.3.6"
|
|
239
243
|
},
|
|
240
244
|
"peerDependencies": {
|
|
241
|
-
"@open-mercato/ai-assistant": "0.6.2-develop.
|
|
242
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
243
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
245
|
+
"@open-mercato/ai-assistant": "0.6.2-develop.3400.2.d948268f50",
|
|
246
|
+
"@open-mercato/shared": "0.6.2-develop.3400.2.d948268f50",
|
|
247
|
+
"@open-mercato/ui": "0.6.2-develop.3400.2.d948268f50",
|
|
244
248
|
"react": "^19.0.0",
|
|
245
249
|
"react-dom": "^19.0.0"
|
|
246
250
|
},
|
|
247
251
|
"devDependencies": {
|
|
248
|
-
"@open-mercato/ai-assistant": "0.6.2-develop.
|
|
249
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
250
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
252
|
+
"@open-mercato/ai-assistant": "0.6.2-develop.3400.2.d948268f50",
|
|
253
|
+
"@open-mercato/shared": "0.6.2-develop.3400.2.d948268f50",
|
|
254
|
+
"@open-mercato/ui": "0.6.2-develop.3400.2.d948268f50",
|
|
251
255
|
"@testing-library/dom": "^10.4.1",
|
|
252
256
|
"@testing-library/jest-dom": "^6.9.1",
|
|
253
257
|
"@testing-library/react": "^16.3.1",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { promises as fs } from "fs";
|
|
3
2
|
import type { OpenApiRouteDoc } from "@open-mercato/shared/lib/openapi";
|
|
4
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
@@ -7,7 +6,6 @@ import {
|
|
|
7
6
|
Attachment,
|
|
8
7
|
AttachmentPartition,
|
|
9
8
|
} from "@open-mercato/core/modules/attachments/data/entities";
|
|
10
|
-
import { resolveAttachmentAbsolutePath } from "@open-mercato/core/modules/attachments/lib/storage";
|
|
11
9
|
import type { EntityManager } from "@mikro-orm/postgresql";
|
|
12
10
|
import { checkAttachmentAccess } from "@open-mercato/core/modules/attachments/lib/access";
|
|
13
11
|
import { z } from "zod";
|
|
@@ -16,6 +14,7 @@ import {
|
|
|
16
14
|
buildAttachmentContentDisposition,
|
|
17
15
|
canRenderInlineAttachment,
|
|
18
16
|
} from "@open-mercato/core/modules/attachments/lib/security";
|
|
17
|
+
import { StorageDriverFactory } from '../../../lib/drivers';
|
|
19
18
|
|
|
20
19
|
export const metadata = {
|
|
21
20
|
GET: { requireAuth: false },
|
|
@@ -35,6 +34,9 @@ export async function GET(
|
|
|
35
34
|
const auth = await getAuthFromRequest(req);
|
|
36
35
|
const { resolve } = await createRequestContainer();
|
|
37
36
|
const em = resolve("em") as EntityManager;
|
|
37
|
+
const storageDriverFactory =
|
|
38
|
+
(resolve("storageDriverFactory") as StorageDriverFactory | null) ??
|
|
39
|
+
new StorageDriverFactory(em);
|
|
38
40
|
|
|
39
41
|
const attachment = await em.findOne(Attachment, { id });
|
|
40
42
|
if (!attachment) {
|
|
@@ -59,14 +61,14 @@ export async function GET(
|
|
|
59
61
|
return NextResponse.json({ error: message }, { status: access.status });
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
const
|
|
63
|
-
attachment.
|
|
64
|
-
attachment.
|
|
65
|
-
|
|
66
|
-
);
|
|
64
|
+
const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
|
|
65
|
+
tenantId: attachment.tenantId ?? '',
|
|
66
|
+
organizationId: attachment.organizationId ?? '',
|
|
67
|
+
});
|
|
67
68
|
let buffer: Buffer;
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
+
const result = await driver.read(attachment.partitionCode, attachment.storagePath);
|
|
71
|
+
buffer = result.buffer;
|
|
70
72
|
} catch {
|
|
71
73
|
return NextResponse.json({ error: "File not available" }, { status: 404 });
|
|
72
74
|
}
|
|
@@ -5,7 +5,6 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
5
5
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
7
|
import { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'
|
|
8
|
-
import { resolveAttachmentAbsolutePath } from '@open-mercato/core/modules/attachments/lib/storage'
|
|
9
8
|
import {
|
|
10
9
|
buildThumbnailCacheKey,
|
|
11
10
|
readThumbnailCache,
|
|
@@ -14,13 +13,13 @@ import {
|
|
|
14
13
|
import { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'
|
|
15
14
|
import { checkAttachmentAccess } from '@open-mercato/core/modules/attachments/lib/access'
|
|
16
15
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
17
|
-
import { promises as fs } from 'fs'
|
|
18
16
|
import { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'
|
|
19
17
|
import {
|
|
20
18
|
MAX_IMAGE_SOURCE_PIXELS,
|
|
21
19
|
validateImageDimensions,
|
|
22
20
|
validateImageMagicBytes,
|
|
23
21
|
} from '@open-mercato/core/modules/attachments/lib/imageSafety'
|
|
22
|
+
import { StorageDriverFactory } from '../../../../lib/drivers'
|
|
24
23
|
|
|
25
24
|
const querySchema = z.object({
|
|
26
25
|
width: z.coerce.number().int().min(1).max(4000).optional(),
|
|
@@ -51,6 +50,8 @@ export async function GET(
|
|
|
51
50
|
|
|
52
51
|
const { resolve } = await createRequestContainer()
|
|
53
52
|
const em = resolve('em') as EntityManager
|
|
53
|
+
const storageDriverFactory =
|
|
54
|
+
(resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
54
55
|
|
|
55
56
|
const attachment = await em.findOne(Attachment, {
|
|
56
57
|
id,
|
|
@@ -71,11 +72,10 @@ export async function GET(
|
|
|
71
72
|
return NextResponse.json({ error: message }, { status: access.status })
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
-
attachment.
|
|
76
|
-
attachment.
|
|
77
|
-
|
|
78
|
-
)
|
|
75
|
+
const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {
|
|
76
|
+
tenantId: attachment.tenantId ?? '',
|
|
77
|
+
organizationId: attachment.organizationId ?? '',
|
|
78
|
+
})
|
|
79
79
|
const cacheKey = buildThumbnailCacheKey(width, height, cropType)
|
|
80
80
|
try {
|
|
81
81
|
let buffer: Buffer | null = null
|
|
@@ -83,7 +83,7 @@ export async function GET(
|
|
|
83
83
|
buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)
|
|
84
84
|
}
|
|
85
85
|
if (!buffer) {
|
|
86
|
-
const input = await
|
|
86
|
+
const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)
|
|
87
87
|
const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)
|
|
88
88
|
if (!magicBytesValidation.ok) {
|
|
89
89
|
return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
normalizeAttachmentTags,
|
|
12
12
|
readAttachmentMetadata,
|
|
13
13
|
} from '../../../lib/metadata'
|
|
14
|
-
import {
|
|
14
|
+
import type { StorageDriverFactory } from '../../../lib/drivers'
|
|
15
15
|
import { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
16
16
|
import { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'
|
|
17
17
|
import { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'
|
|
@@ -228,6 +228,7 @@ export async function DELETE(req: NextRequest, ctx: RouteContext) {
|
|
|
228
228
|
const { resolve } = await createRequestContainer()
|
|
229
229
|
const em = resolve('em') as EntityManager
|
|
230
230
|
const dataEngine = resolve('dataEngine') as DataEngine
|
|
231
|
+
const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory
|
|
231
232
|
const deleteFilter: Record<string, unknown> = {
|
|
232
233
|
id: attachmentId,
|
|
233
234
|
tenantId: auth.tenantId,
|
|
@@ -237,8 +238,11 @@ export async function DELETE(req: NextRequest, ctx: RouteContext) {
|
|
|
237
238
|
if (!record) {
|
|
238
239
|
return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
const deleteDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
242
|
+
tenantId: record.tenantId ?? auth.tenantId,
|
|
243
|
+
organizationId: record.organizationId ?? auth.orgId,
|
|
244
|
+
})
|
|
245
|
+
await deleteDriver.delete(record.partitionCode, record.storagePath)
|
|
242
246
|
await em.remove(record).flush()
|
|
243
247
|
|
|
244
248
|
if (dataEngine) {
|
|
@@ -11,6 +11,7 @@ import { readAttachmentMetadata } from '../../lib/metadata'
|
|
|
11
11
|
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
12
12
|
import { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'
|
|
13
13
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
14
|
+
import { ensureDefaultPartitions } from '../../lib/partitions'
|
|
14
15
|
import {
|
|
15
16
|
attachmentsTag,
|
|
16
17
|
attachmentListQuerySchema as openApiListQuerySchema,
|
|
@@ -70,6 +71,7 @@ export async function GET(req: Request) {
|
|
|
70
71
|
const offset = (page - 1) * pageSize
|
|
71
72
|
const { resolve } = await createRequestContainer()
|
|
72
73
|
const em = resolve('em') as EntityManager
|
|
74
|
+
await ensureDefaultPartitions(em)
|
|
73
75
|
let queryEngine: QueryEngine | null = null
|
|
74
76
|
try {
|
|
75
77
|
queryEngine = resolve('queryEngine') as QueryEngine
|
|
@@ -78,6 +78,8 @@ export const partitionSchema = z.object({
|
|
|
78
78
|
isPublic: z.boolean(),
|
|
79
79
|
requiresOcr: z.boolean(),
|
|
80
80
|
ocrModel: z.string().nullable(),
|
|
81
|
+
storageDriver: z.string().optional().describe('Storage driver: local (default) or s3'),
|
|
82
|
+
configJson: z.record(z.string(), z.unknown()).nullable().optional().describe('Driver-specific configuration'),
|
|
81
83
|
createdAt: z.string().nullable(),
|
|
82
84
|
updatedAt: z.string().nullable(),
|
|
83
85
|
envKey: z.string(),
|
|
@@ -99,6 +101,8 @@ export const partitionCreateSchema = z.object({
|
|
|
99
101
|
isPublic: z.boolean().optional().describe('Whether partition is publicly accessible'),
|
|
100
102
|
requiresOcr: z.boolean().optional().describe('Whether OCR should be performed on uploads'),
|
|
101
103
|
ocrModel: z.string().max(50).nullable().optional().describe('OCR model to use'),
|
|
104
|
+
storageDriver: z.enum(['local', 's3']).optional().default('local').describe('Storage driver to use'),
|
|
105
|
+
configJson: z.record(z.string(), z.unknown()).nullable().optional().describe('Driver-specific configuration (e.g. S3 bucket, region, endpoint)'),
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
export const partitionUpdateSchema = partitionCreateSchema.extend({
|
|
@@ -32,6 +32,8 @@ function serializePartition(entry: AttachmentPartition) {
|
|
|
32
32
|
isPublic: entry.isPublic ?? false,
|
|
33
33
|
requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),
|
|
34
34
|
ocrModel: entry.ocrModel ?? null,
|
|
35
|
+
storageDriver: entry.storageDriver ?? 'local',
|
|
36
|
+
configJson: entry.configJson ?? null,
|
|
35
37
|
createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,
|
|
36
38
|
updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,
|
|
37
39
|
envKey: resolvePartitionEnvKey(entry.code),
|
|
@@ -96,7 +98,8 @@ export async function POST(req: Request) {
|
|
|
96
98
|
code,
|
|
97
99
|
title: parsed.data.title.trim(),
|
|
98
100
|
description: parsed.data.description?.trim() ?? null,
|
|
99
|
-
storageDriver: 'local',
|
|
101
|
+
storageDriver: parsed.data.storageDriver ?? 'local',
|
|
102
|
+
configJson: parsed.data.configJson ?? null,
|
|
100
103
|
isPublic: parsed.data.isPublic ?? false,
|
|
101
104
|
requiresOcr:
|
|
102
105
|
typeof parsed.data.requiresOcr === 'boolean'
|
|
@@ -146,6 +149,12 @@ export async function PUT(req: Request) {
|
|
|
146
149
|
if (parsed.data.ocrModel !== undefined) {
|
|
147
150
|
entry.ocrModel = parsed.data.ocrModel?.trim() || null
|
|
148
151
|
}
|
|
152
|
+
if (parsed.data.storageDriver !== undefined) {
|
|
153
|
+
entry.storageDriver = parsed.data.storageDriver
|
|
154
|
+
}
|
|
155
|
+
if (parsed.data.configJson !== undefined) {
|
|
156
|
+
entry.configJson = parsed.data.configJson ?? null
|
|
157
|
+
}
|
|
149
158
|
await em.persist(entry).flush()
|
|
150
159
|
return NextResponse.json({ item: serializePartition(entry) })
|
|
151
160
|
}
|
|
@@ -7,9 +7,9 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
7
7
|
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'
|
|
8
8
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'
|
|
9
9
|
import { Attachment, AttachmentPartition } from '../data/entities'
|
|
10
|
-
import { storePartitionFile, deletePartitionFile } from '../lib/storage'
|
|
11
10
|
import { extractAttachmentContent } from '../lib/textExtraction'
|
|
12
11
|
import { requestOcrProcessing } from '../lib/ocrQueue'
|
|
12
|
+
import { StorageDriverFactory } from '../lib/drivers'
|
|
13
13
|
import { OcrService, shouldUseLlmOcr } from '../lib/ocrService'
|
|
14
14
|
import { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'
|
|
15
15
|
import {
|
|
@@ -289,6 +289,8 @@ export async function POST(req: Request) {
|
|
|
289
289
|
const { resolve } = await createRequestContainer()
|
|
290
290
|
const em = resolve('em') as EntityManager
|
|
291
291
|
const dataEngine = resolve('dataEngine')
|
|
292
|
+
const storageDriverFactory =
|
|
293
|
+
(resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
292
294
|
await ensureDefaultPartitions(em)
|
|
293
295
|
// Optional per-field validations
|
|
294
296
|
let partitionFromField: string | null = null
|
|
@@ -374,15 +376,17 @@ export async function POST(req: Request) {
|
|
|
374
376
|
if (requestedPublicOverride) {
|
|
375
377
|
return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })
|
|
376
378
|
}
|
|
377
|
-
|
|
379
|
+
const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })
|
|
380
|
+
let storedPath: string
|
|
378
381
|
try {
|
|
379
|
-
stored = await
|
|
382
|
+
const stored = await uploadDriver.store({
|
|
380
383
|
partitionCode: partition.code,
|
|
381
384
|
orgId,
|
|
382
385
|
tenantId,
|
|
383
386
|
fileName: safeName,
|
|
384
387
|
buffer: buf,
|
|
385
388
|
})
|
|
389
|
+
storedPath = stored.storagePath
|
|
386
390
|
} catch (error) {
|
|
387
391
|
console.error('[attachments] failed to persist file', error)
|
|
388
392
|
return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })
|
|
@@ -398,13 +402,16 @@ export async function POST(req: Request) {
|
|
|
398
402
|
const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)
|
|
399
403
|
|
|
400
404
|
if (requiresOcr && !useLlmOcr) {
|
|
405
|
+
const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath)
|
|
401
406
|
try {
|
|
402
407
|
extractedContent = await extractAttachmentContent({
|
|
403
|
-
filePath:
|
|
408
|
+
filePath: localPath,
|
|
404
409
|
mimeType: fileMimeType,
|
|
405
410
|
})
|
|
406
411
|
} catch (error) {
|
|
407
412
|
console.error('[attachments] failed to extract attachment content', error)
|
|
413
|
+
} finally {
|
|
414
|
+
await cleanup().catch(() => {})
|
|
408
415
|
}
|
|
409
416
|
}
|
|
410
417
|
|
|
@@ -425,7 +432,7 @@ export async function POST(req: Request) {
|
|
|
425
432
|
fileSize: buf.length,
|
|
426
433
|
partitionCode: partition.code,
|
|
427
434
|
storageDriver: partition.storageDriver || 'local',
|
|
428
|
-
storagePath:
|
|
435
|
+
storagePath: storedPath,
|
|
429
436
|
url: buildAttachmentFileUrl(attachmentId),
|
|
430
437
|
content: extractedContent,
|
|
431
438
|
storageMetadata: metadata,
|
|
@@ -433,7 +440,7 @@ export async function POST(req: Request) {
|
|
|
433
440
|
await em.persist(att).flush()
|
|
434
441
|
|
|
435
442
|
if (useLlmOcr) {
|
|
436
|
-
requestOcrProcessing(em, att,
|
|
443
|
+
requestOcrProcessing(em, att, uploadDriver, storedPath).catch((error) => {
|
|
437
444
|
console.error('[attachments] failed to queue OCR processing', error)
|
|
438
445
|
})
|
|
439
446
|
} else if (wantsLlmOcr) {
|
|
@@ -519,6 +526,8 @@ export async function DELETE(req: Request) {
|
|
|
519
526
|
const { resolve } = await createRequestContainer()
|
|
520
527
|
const em = resolve('em') as EntityManager
|
|
521
528
|
const dataEngine = resolve('dataEngine')
|
|
529
|
+
const storageDriverFactory =
|
|
530
|
+
(resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
|
|
522
531
|
const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId!, organizationId: auth.orgId }
|
|
523
532
|
const record = await em.findOne(Attachment, deleteFilter)
|
|
524
533
|
if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })
|
|
@@ -527,7 +536,11 @@ export async function DELETE(req: Request) {
|
|
|
527
536
|
console.error('[attachments] failed to cleanup cached thumbnails', error)
|
|
528
537
|
})
|
|
529
538
|
if (record.storagePath) {
|
|
530
|
-
await
|
|
539
|
+
const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
540
|
+
tenantId: record.tenantId ?? auth.tenantId!,
|
|
541
|
+
organizationId: record.organizationId ?? auth.orgId,
|
|
542
|
+
})
|
|
543
|
+
await delDriver.delete(record.partitionCode, record.storagePath)
|
|
531
544
|
}
|
|
532
545
|
if (dataEngine) {
|
|
533
546
|
await emitCrudSideEffects({
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
2
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
3
3
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
4
5
|
import { AttachmentPartitionSettings } from '../../../components/AttachmentPartitionSettings'
|
|
5
6
|
import { isPartitionSettingsLocked } from '../../../lib/partitions'
|
|
6
7
|
|
|
7
8
|
export default 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 @@ export default async function AttachmentsConfigurationPage() {
|
|
|
30
32
|
</AlertDescription>
|
|
31
33
|
</Alert>
|
|
32
34
|
) : null}
|
|
33
|
-
{!partitionsLocked ? <AttachmentPartitionSettings /> : null}
|
|
35
|
+
{!partitionsLocked ? <AttachmentPartitionSettings s3Enabled={s3Enabled} /> : null}
|
|
34
36
|
</PageBody>
|
|
35
37
|
</Page>
|
|
36
38
|
)
|
|
@@ -2,7 +2,7 @@ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
|
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { Attachment } from './data/entities'
|
|
5
|
-
import {
|
|
5
|
+
import type { StorageDriverFactory } from './lib/drivers'
|
|
6
6
|
|
|
7
7
|
type ParsedArgs = Record<string, string | boolean>
|
|
8
8
|
|
|
@@ -65,6 +65,7 @@ const deleteAttachments: ModuleCli = {
|
|
|
65
65
|
try {
|
|
66
66
|
const container = await createRequestContainer()
|
|
67
67
|
const em = container.resolve<EntityManager>('em')
|
|
68
|
+
const storageDriverFactory = container.resolve<StorageDriverFactory>('storageDriverFactory')
|
|
68
69
|
const idList = Array.from(ids)
|
|
69
70
|
const where: Record<string, unknown> = { id: { $in: idList } }
|
|
70
71
|
if (organizationId) where.organizationId = organizationId
|
|
@@ -76,7 +77,11 @@ const deleteAttachments: ModuleCli = {
|
|
|
76
77
|
}
|
|
77
78
|
const removedIds = new Set<string>()
|
|
78
79
|
for (const entry of attachments) {
|
|
79
|
-
await
|
|
80
|
+
const driver = await storageDriverFactory.resolveForPartition(entry.partitionCode, {
|
|
81
|
+
tenantId: entry.tenantId ?? '',
|
|
82
|
+
organizationId: entry.organizationId ?? '',
|
|
83
|
+
})
|
|
84
|
+
await driver.delete(entry.partitionCode, entry.storagePath)
|
|
80
85
|
em.remove(entry)
|
|
81
86
|
removedIds.add(entry.id)
|
|
82
87
|
console.log(`Deleted attachment ${entry.id}${entry.fileName ? ` (${entry.fileName})` : ''}`)
|