@open-mercato/core 0.6.2-develop.3380.1.bb14509041 → 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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +8 -8
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -8
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/attachments/api/library/[id]/route.js +6 -2
  7. package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
  8. package/dist/modules/attachments/api/library/route.js +2 -0
  9. package/dist/modules/attachments/api/library/route.js.map +2 -2
  10. package/dist/modules/attachments/api/openapi.js +5 -1
  11. package/dist/modules/attachments/api/openapi.js.map +2 -2
  12. package/dist/modules/attachments/api/partitions/route.js +10 -1
  13. package/dist/modules/attachments/api/partitions/route.js.map +2 -2
  14. package/dist/modules/attachments/api/route.js +19 -7
  15. package/dist/modules/attachments/api/route.js.map +2 -2
  16. package/dist/modules/attachments/backend/config/attachments/page.js +3 -1
  17. package/dist/modules/attachments/backend/config/attachments/page.js.map +2 -2
  18. package/dist/modules/attachments/cli.js +6 -2
  19. package/dist/modules/attachments/cli.js.map +2 -2
  20. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +216 -10
  21. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  22. package/dist/modules/attachments/di.js +13 -0
  23. package/dist/modules/attachments/di.js.map +7 -0
  24. package/dist/modules/attachments/lib/drivers/driverFactory.js +67 -0
  25. package/dist/modules/attachments/lib/drivers/driverFactory.js.map +7 -0
  26. package/dist/modules/attachments/lib/drivers/index.js +11 -0
  27. package/dist/modules/attachments/lib/drivers/index.js.map +7 -0
  28. package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js +42 -0
  29. package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js.map +7 -0
  30. package/dist/modules/attachments/lib/drivers/localDriver.js +70 -0
  31. package/dist/modules/attachments/lib/drivers/localDriver.js.map +7 -0
  32. package/dist/modules/attachments/lib/drivers/types.js +1 -0
  33. package/dist/modules/attachments/lib/drivers/types.js.map +7 -0
  34. package/dist/modules/attachments/lib/ocrQueue.js +10 -5
  35. package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
  36. package/dist/modules/attachments/lib/storage.js.map +2 -2
  37. package/dist/modules/attachments/lib/thumbnailCache.js +11 -13
  38. package/dist/modules/attachments/lib/thumbnailCache.js.map +2 -2
  39. package/dist/modules/integrations/backend/integrations/[id]/page.js +31 -5
  40. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  41. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +14 -2
  42. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  43. package/package.json +11 -7
  44. package/src/modules/attachments/api/file/[id]/route.ts +10 -8
  45. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +8 -8
  46. package/src/modules/attachments/api/library/[id]/route.ts +7 -3
  47. package/src/modules/attachments/api/library/route.ts +2 -0
  48. package/src/modules/attachments/api/openapi.ts +4 -0
  49. package/src/modules/attachments/api/partitions/route.ts +10 -1
  50. package/src/modules/attachments/api/route.ts +20 -7
  51. package/src/modules/attachments/backend/config/attachments/page.tsx +3 -1
  52. package/src/modules/attachments/cli.ts +7 -2
  53. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +275 -7
  54. package/src/modules/attachments/di.ts +13 -0
  55. package/src/modules/attachments/lib/drivers/driverFactory.ts +91 -0
  56. package/src/modules/attachments/lib/drivers/index.ts +4 -0
  57. package/src/modules/attachments/lib/drivers/legacyPublicDriver.ts +46 -0
  58. package/src/modules/attachments/lib/drivers/localDriver.ts +79 -0
  59. package/src/modules/attachments/lib/drivers/types.ts +28 -0
  60. package/src/modules/attachments/lib/ocrQueue.ts +14 -6
  61. package/src/modules/attachments/lib/storage.ts +13 -0
  62. package/src/modules/attachments/lib/thumbnailCache.ts +25 -19
  63. package/src/modules/integrations/backend/integrations/[id]/page.tsx +30 -4
  64. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +15 -2
@@ -1,10 +1,11 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { Attachment, AttachmentPartition } from '../data/entities'
3
3
  import { OcrService } from './ocrService'
4
+ import type { StorageDriver } from './drivers/types'
4
5
 
5
6
  export type OcrRequestedEvent = {
6
7
  attachmentId: string
7
- filePath: string
8
+ storagePath: string
8
9
  mimeType: string
9
10
  partitionCode: string
10
11
  organizationId: string | null
@@ -13,13 +14,15 @@ export type OcrRequestedEvent = {
13
14
 
14
15
  export async function processAttachmentOcr(
15
16
  em: EntityManager,
16
- payload: OcrRequestedEvent
17
+ payload: OcrRequestedEvent,
18
+ driver: StorageDriver,
17
19
  ): Promise<void> {
18
- const { attachmentId, filePath, mimeType, partitionCode } = payload
20
+ const { attachmentId, storagePath, mimeType, partitionCode } = payload
19
21
 
20
22
  console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`)
21
23
  const startTime = Date.now()
22
24
 
25
+ const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)
23
26
  try {
24
27
  const partition = await em.findOne(AttachmentPartition, { code: partitionCode })
25
28
  const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'
@@ -63,17 +66,22 @@ export async function processAttachmentOcr(
63
66
  attachmentId,
64
67
  error: error instanceof Error ? error.message : String(error),
65
68
  })
69
+ } finally {
70
+ await cleanup().catch((cleanupError) => {
71
+ console.warn(`[attachments.ocr] Temp file cleanup failed:`, cleanupError)
72
+ })
66
73
  }
67
74
  }
68
75
 
69
76
  export async function requestOcrProcessing(
70
77
  em: EntityManager,
71
78
  attachment: Attachment,
72
- filePath: string
79
+ driver: StorageDriver,
80
+ storagePath: string,
73
81
  ): Promise<void> {
74
82
  const payload: OcrRequestedEvent = {
75
83
  attachmentId: attachment.id,
76
- filePath,
84
+ storagePath,
77
85
  mimeType: attachment.mimeType,
78
86
  partitionCode: attachment.partitionCode,
79
87
  organizationId: attachment.organizationId ?? null,
@@ -82,7 +90,7 @@ export async function requestOcrProcessing(
82
90
 
83
91
  setImmediate(() => {
84
92
  const workerEm = typeof (em as any)?.fork === 'function' ? (em as any).fork() : em
85
- processAttachmentOcr(workerEm, payload).catch((error) => {
93
+ processAttachmentOcr(workerEm, payload, driver).catch((error) => {
86
94
  console.error(`[attachments.ocr] Background processing error:`, error)
87
95
  })
88
96
  })
@@ -27,6 +27,10 @@ function resolveTenantSegment(tenantId: string | null | undefined): string {
27
27
  return 'tenant_shared'
28
28
  }
29
29
 
30
+ /**
31
+ * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.
32
+ * Kept for backward compatibility with external callers.
33
+ */
30
34
  export type StorePartitionFilePayload = {
31
35
  partitionCode: string
32
36
  orgId: string | null | undefined
@@ -41,6 +45,9 @@ export type StoredPartitionFile = {
41
45
  fileName: string
42
46
  }
43
47
 
48
+ /**
49
+ * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.
50
+ */
44
51
  export async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {
45
52
  const root = resolvePartitionRoot(payload.partitionCode)
46
53
  const orgSegment = resolveOrgSegment(payload.orgId ?? null)
@@ -59,6 +66,9 @@ export async function storePartitionFile(payload: StorePartitionFilePayload): Pr
59
66
  }
60
67
  }
61
68
 
69
+ /**
70
+ * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.
71
+ */
62
72
  export function resolveAttachmentAbsolutePath(
63
73
  partitionCode: string,
64
74
  storagePath: string,
@@ -78,6 +88,9 @@ export function resolveAttachmentAbsolutePath(
78
88
  return path.join(root, safeRelative)
79
89
  }
80
90
 
91
+ /**
92
+ * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.
93
+ */
81
94
  export async function deletePartitionFile(
82
95
  partitionCode: string,
83
96
  storagePath: string,
@@ -1,17 +1,31 @@
1
1
  import { promises as fs } from 'fs'
2
2
  import path from 'path'
3
- import { resolvePartitionRoot } from './storage'
4
-
5
- const CACHE_ROOT_SEGMENTS = ['.cache', 'thumbnails']
6
3
 
7
4
  function sanitizeSegment(value: string): string {
8
5
  return value.replace(/[^a-zA-Z0-9._-]/g, '_')
9
6
  }
10
7
 
8
+ /**
9
+ * Resolves the thumbnail cache root. Uses a dedicated directory under
10
+ * `storage/.cache/thumbnails/<partitionCode>/` that is independent of
11
+ * the partition's storage backend (local or S3).
12
+ */
13
+ function resolveCacheRoot(partitionCode: string): string {
14
+ return path.join(process.cwd(), 'storage', '.cache', 'thumbnails', sanitizeSegment(partitionCode))
15
+ }
16
+
17
+ function resolveCachePath(partitionCode: string, attachmentId: string, cacheKey: string): string {
18
+ return path.join(
19
+ resolveCacheRoot(partitionCode),
20
+ sanitizeSegment(attachmentId),
21
+ sanitizeSegment(cacheKey),
22
+ )
23
+ }
24
+
11
25
  export function buildThumbnailCacheKey(
12
26
  width?: number,
13
27
  height?: number,
14
- cropType?: 'cover' | 'contain'
28
+ cropType?: 'cover' | 'contain',
15
29
  ): string | null {
16
30
  if (!width && !height) return null
17
31
  const safeWidth = typeof width === 'number' && Number.isFinite(width) ? width : 'auto'
@@ -20,20 +34,10 @@ export function buildThumbnailCacheKey(
20
34
  return `w${safeWidth}-h${safeHeight}-c${safeCrop}`
21
35
  }
22
36
 
23
- function resolveCachePath(partitionCode: string, attachmentId: string, cacheKey: string): string {
24
- const root = resolvePartitionRoot(partitionCode)
25
- return path.join(
26
- root,
27
- ...CACHE_ROOT_SEGMENTS,
28
- sanitizeSegment(attachmentId),
29
- sanitizeSegment(cacheKey)
30
- )
31
- }
32
-
33
37
  export async function readThumbnailCache(
34
38
  partitionCode: string,
35
39
  attachmentId: string,
36
- cacheKey: string
40
+ cacheKey: string,
37
41
  ): Promise<Buffer | null> {
38
42
  const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)
39
43
  try {
@@ -47,16 +51,18 @@ export async function writeThumbnailCache(
47
51
  partitionCode: string,
48
52
  attachmentId: string,
49
53
  cacheKey: string,
50
- data: Buffer
54
+ data: Buffer,
51
55
  ): Promise<void> {
52
56
  const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)
53
57
  await fs.mkdir(path.dirname(cachePath), { recursive: true })
54
58
  await fs.writeFile(cachePath, data)
55
59
  }
56
60
 
57
- export async function clearAttachmentThumbnailCache(partitionCode: string, attachmentId: string): Promise<void> {
58
- const root = resolvePartitionRoot(partitionCode)
59
- const dir = path.join(root, ...CACHE_ROOT_SEGMENTS, sanitizeSegment(attachmentId))
61
+ export async function clearAttachmentThumbnailCache(
62
+ partitionCode: string,
63
+ attachmentId: string,
64
+ ): Promise<void> {
65
+ const dir = path.join(resolveCacheRoot(partitionCode), sanitizeSegment(attachmentId))
60
66
  try {
61
67
  await fs.rm(dir, { recursive: true, force: true })
62
68
  } catch {
@@ -295,6 +295,7 @@ function buildCredentialFields(credFields: CredentialField[]): CrudField[] {
295
295
  ) : field.helpText,
296
296
  placeholder: field.placeholder,
297
297
  required: field.required,
298
+ visibleWhen: field.visibleWhen,
298
299
  }
299
300
 
300
301
  if (field.type === 'secret') {
@@ -478,7 +479,15 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
478
479
  { fallback: null },
479
480
  )
480
481
  if (call.ok && call.result?.credentials) {
481
- setCredValues(call.result.credentials)
482
+ const next = { ...call.result.credentials }
483
+ if (currentIntegrationId === 'storage_s3') {
484
+ const authMode = next.authMode
485
+ if (authMode !== 'access_keys' && authMode !== 'ambient') {
486
+ const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)
487
+ next.authMode = hasKeys ? 'access_keys' : 'ambient'
488
+ }
489
+ }
490
+ setCredValues(next)
482
491
  setCredentialsFormKey((current) => current + 1)
483
492
  }
484
493
  }, [resolveCurrentIntegrationId])
@@ -680,19 +689,32 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
680
689
  if (!currentIntegrationId) return
681
690
  setIsSavingCredentials(true)
682
691
  try {
692
+ const sanitizedValues = { ...values }
693
+ if (currentIntegrationId === 'storage_s3') {
694
+ const authMode = sanitizedValues.authMode
695
+ if (authMode !== 'access_keys' && authMode !== 'ambient') {
696
+ const hasKeys = Boolean(sanitizedValues.accessKeyId || sanitizedValues.secretAccessKey)
697
+ sanitizedValues.authMode = hasKeys ? 'access_keys' : 'ambient'
698
+ }
699
+ if (sanitizedValues.authMode === 'ambient') {
700
+ delete sanitizedValues.accessKeyId
701
+ delete sanitizedValues.secretAccessKey
702
+ delete sanitizedValues.sessionToken
703
+ }
704
+ }
683
705
  const call = await runMutationWithContext({
684
706
  actionId: 'save-credentials',
685
707
  tabId: 'credentials',
686
- mutationPayload: { integrationId: currentIntegrationId, credentials: values },
708
+ mutationPayload: { integrationId: currentIntegrationId, credentials: sanitizedValues },
687
709
  operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
688
710
  method: 'PUT',
689
711
  headers: { 'Content-Type': 'application/json' },
690
- body: JSON.stringify({ credentials: values }),
712
+ body: JSON.stringify({ credentials: sanitizedValues }),
691
713
  }, { fallback: null }),
692
714
  })
693
715
 
694
716
  if (call.ok) {
695
- setCredValues(values)
717
+ setCredValues(sanitizedValues)
696
718
  setCredentialsFormKey((current) => current + 1)
697
719
  flash(t('integrations.detail.credentials.saved'), 'success')
698
720
  return
@@ -792,6 +814,10 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
792
814
  const values = rawValues as Record<string, unknown>
793
815
 
794
816
  editableCredentialFields.forEach((field) => {
817
+ if (field.visibleWhen) {
818
+ const targetValue = values[field.visibleWhen.field]
819
+ if (targetValue !== field.visibleWhen.equals) return
820
+ }
795
821
  const value = values[field.key]
796
822
 
797
823
  if (field.type === 'boolean') {
@@ -122,7 +122,15 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
122
122
  { fallback: null },
123
123
  )
124
124
  if (credCall.ok && credCall.result?.credentials) {
125
- setCredValues(credCall.result.credentials)
125
+ const next = { ...credCall.result.credentials }
126
+ if (currentBundleId === 'storage_s3') {
127
+ const authMode = next.authMode
128
+ if (authMode !== 'access_keys' && authMode !== 'ambient') {
129
+ const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey)
130
+ next.authMode = hasKeys ? 'access_keys' : 'ambient'
131
+ }
132
+ }
133
+ setCredValues(next)
126
134
  }
127
135
  setIsLoading(false)
128
136
  }, [resolveCurrentBundleId, t])
@@ -180,6 +188,11 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
180
188
 
181
189
  const credFields = (detail.bundle.credentials?.fields ?? []).filter(isEditableCredentialField)
182
190
 
191
+ function isFieldVisible(field: CredentialField): boolean {
192
+ if (!field.visibleWhen) return true
193
+ return credValues[field.visibleWhen.field] === field.visibleWhen.equals
194
+ }
195
+
183
196
  return (
184
197
  <Page>
185
198
  <PageBody className="space-y-6">
@@ -202,7 +215,7 @@ export default function BundleConfigPage({ params }: BundleConfigPageProps) {
202
215
  <CardTitle>{t('integrations.bundle.sharedCredentials')}</CardTitle>
203
216
  </CardHeader>
204
217
  <CardContent className="space-y-4">
205
- {credFields.map((field) => (
218
+ {credFields.filter(isFieldVisible).map((field) => (
206
219
  <div key={field.key} className="space-y-1.5">
207
220
  <label className="text-sm font-medium">
208
221
  {field.label}{field.required && <span className="text-red-500 ml-0.5">*</span>}