@open-mercato/core 0.4.6-develop-6d72ec5960 → 0.4.6-develop-cd1e2a9a0e

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 (226) hide show
  1. package/AGENTS.md +10 -0
  2. package/dist/generated/entities/integration_credentials/index.js +19 -0
  3. package/dist/generated/entities/integration_credentials/index.js.map +7 -0
  4. package/dist/generated/entities/integration_log/index.js +27 -0
  5. package/dist/generated/entities/integration_log/index.js.map +7 -0
  6. package/dist/generated/entities/integration_state/index.js +27 -0
  7. package/dist/generated/entities/integration_state/index.js.map +7 -0
  8. package/dist/generated/entities/sync_cursor/index.js +19 -0
  9. package/dist/generated/entities/sync_cursor/index.js.map +7 -0
  10. package/dist/generated/entities/sync_external_id_mapping/index.js +27 -0
  11. package/dist/generated/entities/sync_external_id_mapping/index.js.map +7 -0
  12. package/dist/generated/entities/sync_mapping/index.js +19 -0
  13. package/dist/generated/entities/sync_mapping/index.js.map +7 -0
  14. package/dist/generated/entities/sync_run/index.js +45 -0
  15. package/dist/generated/entities/sync_run/index.js.map +7 -0
  16. package/dist/generated/entities/sync_schedule/index.js +35 -0
  17. package/dist/generated/entities/sync_schedule/index.js.map +7 -0
  18. package/dist/generated/entities.ids.generated.js +14 -0
  19. package/dist/generated/entities.ids.generated.js.map +2 -2
  20. package/dist/generated/entity-fields-registry.js +16 -0
  21. package/dist/generated/entity-fields-registry.js.map +2 -2
  22. package/dist/modules/data_sync/acl.js +11 -0
  23. package/dist/modules/data_sync/acl.js.map +7 -0
  24. package/dist/modules/data_sync/api/mappings/[id]/route.js +137 -0
  25. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +7 -0
  26. package/dist/modules/data_sync/api/mappings/route.js +132 -0
  27. package/dist/modules/data_sync/api/mappings/route.js.map +7 -0
  28. package/dist/modules/data_sync/api/run.js +87 -0
  29. package/dist/modules/data_sync/api/run.js.map +7 -0
  30. package/dist/modules/data_sync/api/runs/[id]/cancel.js +49 -0
  31. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +7 -0
  32. package/dist/modules/data_sync/api/runs/[id]/retry.js +93 -0
  33. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +7 -0
  34. package/dist/modules/data_sync/api/runs/[id]/route.js +69 -0
  35. package/dist/modules/data_sync/api/runs/[id]/route.js.map +7 -0
  36. package/dist/modules/data_sync/api/runs.js +66 -0
  37. package/dist/modules/data_sync/api/runs.js.map +7 -0
  38. package/dist/modules/data_sync/api/validate.js +66 -0
  39. package/dist/modules/data_sync/api/validate.js.map +7 -0
  40. package/dist/modules/data_sync/backend/data-sync/page.js +216 -0
  41. package/dist/modules/data_sync/backend/data-sync/page.js.map +7 -0
  42. package/dist/modules/data_sync/backend/data-sync/page.meta.js +25 -0
  43. package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +7 -0
  44. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +178 -0
  45. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +7 -0
  46. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js +14 -0
  47. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.meta.js.map +7 -0
  48. package/dist/modules/data_sync/data/entities.js +228 -0
  49. package/dist/modules/data_sync/data/entities.js.map +7 -0
  50. package/dist/modules/data_sync/data/validators.js +32 -0
  51. package/dist/modules/data_sync/data/validators.js.map +7 -0
  52. package/dist/modules/data_sync/di.js +26 -0
  53. package/dist/modules/data_sync/di.js.map +7 -0
  54. package/dist/modules/data_sync/events.js +16 -0
  55. package/dist/modules/data_sync/events.js.map +7 -0
  56. package/dist/modules/data_sync/index.js +9 -0
  57. package/dist/modules/data_sync/index.js.map +7 -0
  58. package/dist/modules/data_sync/lib/adapter-registry.js +16 -0
  59. package/dist/modules/data_sync/lib/adapter-registry.js.map +7 -0
  60. package/dist/modules/data_sync/lib/adapter.js +1 -0
  61. package/dist/modules/data_sync/lib/adapter.js.map +7 -0
  62. package/dist/modules/data_sync/lib/id-mapping.js +79 -0
  63. package/dist/modules/data_sync/lib/id-mapping.js.map +7 -0
  64. package/dist/modules/data_sync/lib/queue.js +17 -0
  65. package/dist/modules/data_sync/lib/queue.js.map +7 -0
  66. package/dist/modules/data_sync/lib/sync-engine.js +309 -0
  67. package/dist/modules/data_sync/lib/sync-engine.js.map +7 -0
  68. package/dist/modules/data_sync/lib/sync-run-service.js +148 -0
  69. package/dist/modules/data_sync/lib/sync-run-service.js.map +7 -0
  70. package/dist/modules/data_sync/migrations/Migration20260304113737.js +17 -0
  71. package/dist/modules/data_sync/migrations/Migration20260304113737.js.map +7 -0
  72. package/dist/modules/data_sync/setup.js +13 -0
  73. package/dist/modules/data_sync/setup.js.map +7 -0
  74. package/dist/modules/data_sync/workers/sync-export.js +14 -0
  75. package/dist/modules/data_sync/workers/sync-export.js.map +7 -0
  76. package/dist/modules/data_sync/workers/sync-import.js +14 -0
  77. package/dist/modules/data_sync/workers/sync-import.js.map +7 -0
  78. package/dist/modules/data_sync/workers/sync-scheduled.js +63 -0
  79. package/dist/modules/data_sync/workers/sync-scheduled.js.map +7 -0
  80. package/dist/modules/entities/lib/encryptionDefaults.js +4 -0
  81. package/dist/modules/entities/lib/encryptionDefaults.js.map +2 -2
  82. package/dist/modules/integrations/acl.js +4 -1
  83. package/dist/modules/integrations/acl.js.map +2 -2
  84. package/dist/modules/integrations/api/[id]/credentials/route.js +127 -0
  85. package/dist/modules/integrations/api/[id]/credentials/route.js.map +7 -0
  86. package/dist/modules/integrations/api/[id]/health/route.js +46 -0
  87. package/dist/modules/integrations/api/[id]/health/route.js.map +7 -0
  88. package/dist/modules/integrations/api/[id]/route.js +65 -0
  89. package/dist/modules/integrations/api/[id]/route.js.map +7 -0
  90. package/dist/modules/integrations/api/[id]/state/route.js +109 -0
  91. package/dist/modules/integrations/api/[id]/state/route.js.map +7 -0
  92. package/dist/modules/integrations/api/[id]/version/route.js +117 -0
  93. package/dist/modules/integrations/api/[id]/version/route.js.map +7 -0
  94. package/dist/modules/integrations/api/guards.js +31 -0
  95. package/dist/modules/integrations/api/guards.js.map +7 -0
  96. package/dist/modules/integrations/api/logs/route.js +60 -0
  97. package/dist/modules/integrations/api/logs/route.js.map +7 -0
  98. package/dist/modules/integrations/api/openapi.js +25 -0
  99. package/dist/modules/integrations/api/openapi.js.map +7 -0
  100. package/dist/modules/integrations/api/route.js +68 -0
  101. package/dist/modules/integrations/api/route.js.map +7 -0
  102. package/dist/modules/integrations/backend/integrations/[id]/page.js +313 -0
  103. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +7 -0
  104. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js +15 -0
  105. package/dist/modules/integrations/backend/integrations/[id]/page.meta.js.map +7 -0
  106. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +189 -0
  107. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +7 -0
  108. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js +15 -0
  109. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.meta.js.map +7 -0
  110. package/dist/modules/integrations/backend/integrations/page.js +212 -0
  111. package/dist/modules/integrations/backend/integrations/page.js.map +7 -0
  112. package/dist/modules/integrations/backend/integrations/page.meta.js +22 -0
  113. package/dist/modules/integrations/backend/integrations/page.meta.js.map +7 -0
  114. package/dist/modules/integrations/data/enrichers.js +27 -12
  115. package/dist/modules/integrations/data/enrichers.js.map +2 -2
  116. package/dist/modules/integrations/data/entities.js +136 -1
  117. package/dist/modules/integrations/data/entities.js.map +2 -2
  118. package/dist/modules/integrations/data/validators.js +36 -0
  119. package/dist/modules/integrations/data/validators.js.map +7 -0
  120. package/dist/modules/integrations/di.js +24 -0
  121. package/dist/modules/integrations/di.js.map +7 -0
  122. package/dist/modules/integrations/events.js +19 -0
  123. package/dist/modules/integrations/events.js.map +7 -0
  124. package/dist/modules/integrations/lib/credentials-service.js +159 -0
  125. package/dist/modules/integrations/lib/credentials-service.js.map +7 -0
  126. package/dist/modules/integrations/lib/health-service.js +37 -0
  127. package/dist/modules/integrations/lib/health-service.js.map +7 -0
  128. package/dist/modules/integrations/lib/log-service.js +66 -0
  129. package/dist/modules/integrations/lib/log-service.js.map +7 -0
  130. package/dist/modules/integrations/lib/registry-service.js +33 -0
  131. package/dist/modules/integrations/lib/registry-service.js.map +7 -0
  132. package/dist/modules/integrations/lib/state-service.js +55 -0
  133. package/dist/modules/integrations/lib/state-service.js.map +7 -0
  134. package/dist/modules/integrations/lib/types.js +1 -0
  135. package/dist/modules/integrations/lib/types.js.map +7 -0
  136. package/dist/modules/integrations/migrations/Migration20260304113737.js +19 -0
  137. package/dist/modules/integrations/migrations/Migration20260304113737.js.map +7 -0
  138. package/dist/modules/integrations/setup.js +2 -2
  139. package/dist/modules/integrations/setup.js.map +2 -2
  140. package/dist/modules/integrations/widgets/injection-table.js.map +1 -1
  141. package/dist/modules/integrations/workers/log-pruner.js +18 -0
  142. package/dist/modules/integrations/workers/log-pruner.js.map +7 -0
  143. package/generated/entities/integration_credentials/index.ts +8 -0
  144. package/generated/entities/integration_log/index.ts +12 -0
  145. package/generated/entities/integration_state/index.ts +12 -0
  146. package/generated/entities/sync_cursor/index.ts +8 -0
  147. package/generated/entities/sync_external_id_mapping/index.ts +12 -0
  148. package/generated/entities/sync_mapping/index.ts +8 -0
  149. package/generated/entities/sync_run/index.ts +21 -0
  150. package/generated/entities/sync_schedule/index.ts +16 -0
  151. package/generated/entities.ids.generated.ts +14 -0
  152. package/generated/entity-fields-registry.ts +16 -0
  153. package/package.json +2 -2
  154. package/src/modules/data_sync/AGENTS.md +157 -0
  155. package/src/modules/data_sync/acl.ts +7 -0
  156. package/src/modules/data_sync/api/mappings/[id]/route.ts +158 -0
  157. package/src/modules/data_sync/api/mappings/route.ts +144 -0
  158. package/src/modules/data_sync/api/run.ts +97 -0
  159. package/src/modules/data_sync/api/runs/[id]/cancel.ts +57 -0
  160. package/src/modules/data_sync/api/runs/[id]/retry.ts +108 -0
  161. package/src/modules/data_sync/api/runs/[id]/route.ts +81 -0
  162. package/src/modules/data_sync/api/runs.ts +69 -0
  163. package/src/modules/data_sync/api/validate.ts +73 -0
  164. package/src/modules/data_sync/backend/data-sync/page.meta.ts +21 -0
  165. package/src/modules/data_sync/backend/data-sync/page.tsx +244 -0
  166. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.meta.ts +10 -0
  167. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +278 -0
  168. package/src/modules/data_sync/data/entities.ts +180 -0
  169. package/src/modules/data_sync/data/validators.ts +35 -0
  170. package/src/modules/data_sync/di.ts +38 -0
  171. package/src/modules/data_sync/events.ts +12 -0
  172. package/src/modules/data_sync/i18n/de.json +48 -0
  173. package/src/modules/data_sync/i18n/en.json +48 -0
  174. package/src/modules/data_sync/i18n/es.json +48 -0
  175. package/src/modules/data_sync/i18n/pl.json +48 -0
  176. package/src/modules/data_sync/index.ts +5 -0
  177. package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
  178. package/src/modules/data_sync/lib/adapter.ts +90 -0
  179. package/src/modules/data_sync/lib/id-mapping.ts +95 -0
  180. package/src/modules/data_sync/lib/queue.ts +19 -0
  181. package/src/modules/data_sync/lib/sync-engine.ts +375 -0
  182. package/src/modules/data_sync/lib/sync-run-service.ts +187 -0
  183. package/src/modules/data_sync/migrations/.snapshot-open-mercato.json +653 -0
  184. package/src/modules/data_sync/migrations/Migration20260304113737.ts +19 -0
  185. package/src/modules/data_sync/setup.ts +11 -0
  186. package/src/modules/data_sync/workers/sync-export.ts +27 -0
  187. package/src/modules/data_sync/workers/sync-import.ts +27 -0
  188. package/src/modules/data_sync/workers/sync-scheduled.ts +84 -0
  189. package/src/modules/entities/lib/encryptionDefaults.ts +4 -0
  190. package/src/modules/integrations/AGENTS.md +160 -0
  191. package/src/modules/integrations/acl.ts +3 -0
  192. package/src/modules/integrations/api/[id]/credentials/route.ts +142 -0
  193. package/src/modules/integrations/api/[id]/health/route.ts +53 -0
  194. package/src/modules/integrations/api/[id]/route.ts +76 -0
  195. package/src/modules/integrations/api/[id]/state/route.ts +121 -0
  196. package/src/modules/integrations/api/[id]/version/route.ts +132 -0
  197. package/src/modules/integrations/api/guards.ts +59 -0
  198. package/src/modules/integrations/api/logs/route.ts +63 -0
  199. package/src/modules/integrations/api/openapi.ts +22 -0
  200. package/src/modules/integrations/api/route.ts +73 -0
  201. package/src/modules/integrations/backend/integrations/[id]/page.meta.ts +11 -0
  202. package/src/modules/integrations/backend/integrations/[id]/page.tsx +424 -0
  203. package/src/modules/integrations/backend/integrations/bundle/[id]/page.meta.ts +11 -0
  204. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +249 -0
  205. package/src/modules/integrations/backend/integrations/page.meta.ts +18 -0
  206. package/src/modules/integrations/backend/integrations/page.tsx +296 -0
  207. package/src/modules/integrations/data/enrichers.ts +35 -18
  208. package/src/modules/integrations/data/entities.ts +114 -5
  209. package/src/modules/integrations/data/validators.ts +41 -0
  210. package/src/modules/integrations/di.ts +31 -0
  211. package/src/modules/integrations/events.ts +17 -0
  212. package/src/modules/integrations/i18n/de.json +70 -0
  213. package/src/modules/integrations/i18n/en.json +70 -0
  214. package/src/modules/integrations/i18n/es.json +70 -0
  215. package/src/modules/integrations/i18n/pl.json +70 -0
  216. package/src/modules/integrations/lib/credentials-service.ts +204 -0
  217. package/src/modules/integrations/lib/health-service.ts +59 -0
  218. package/src/modules/integrations/lib/log-service.ts +84 -0
  219. package/src/modules/integrations/lib/registry-service.ts +42 -0
  220. package/src/modules/integrations/lib/state-service.ts +64 -0
  221. package/src/modules/integrations/lib/types.ts +4 -0
  222. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +582 -0
  223. package/src/modules/integrations/migrations/Migration20260304113737.ts +21 -0
  224. package/src/modules/integrations/setup.ts +2 -2
  225. package/src/modules/integrations/widgets/injection-table.ts +1 -1
  226. package/src/modules/integrations/workers/log-pruner.ts +30 -0
@@ -0,0 +1,18 @@
1
+ import React from 'react'
2
+
3
+ const puzzleIcon = React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },
4
+ React.createElement('path', { d: 'M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z' }),
5
+ )
6
+
7
+ export const metadata = {
8
+ requireAuth: true,
9
+ requireFeatures: ['integrations.view'],
10
+ pageTitle: 'Integrations',
11
+ pageTitleKey: 'integrations.nav.title',
12
+ pageGroup: 'Settings',
13
+ pageGroupKey: 'settings.sections.general',
14
+ pageOrder: 50,
15
+ icon: puzzleIcon,
16
+ pageContext: 'settings' as const,
17
+ breadcrumb: [{ label: 'Integrations', labelKey: 'integrations.nav.title' }],
18
+ }
@@ -0,0 +1,296 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
6
+ import { Badge } from '@open-mercato/ui/primitives/badge'
7
+ import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Switch } from '@open-mercato/ui/primitives/switch'
9
+ import { Input } from '@open-mercato/ui/primitives/input'
10
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
11
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
12
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
+ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
14
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import {
16
+ LayoutGrid,
17
+ CreditCard,
18
+ Truck,
19
+ RefreshCw,
20
+ MessageSquare,
21
+ Bell,
22
+ HardDrive,
23
+ Webhook,
24
+ } from 'lucide-react'
25
+
26
+ type IntegrationItem = {
27
+ id: string
28
+ title: string
29
+ description?: string
30
+ category?: string
31
+ icon?: string
32
+ bundleId?: string
33
+ isEnabled: boolean
34
+ hasCredentials: boolean
35
+ }
36
+
37
+ type BundleItem = {
38
+ id: string
39
+ title: string
40
+ description?: string
41
+ icon?: string
42
+ integrationCount: number
43
+ enabledCount: number
44
+ }
45
+
46
+ type ListResponse = {
47
+ items: IntegrationItem[]
48
+ bundles: BundleItem[]
49
+ }
50
+
51
+ const CATEGORY_ICONS: Record<string, React.ElementType> = {
52
+ all: LayoutGrid,
53
+ payment: CreditCard,
54
+ shipping: Truck,
55
+ data_sync: RefreshCw,
56
+ communication: MessageSquare,
57
+ notification: Bell,
58
+ storage: HardDrive,
59
+ webhook: Webhook,
60
+ }
61
+
62
+ const CATEGORIES = ['all', 'payment', 'shipping', 'data_sync', 'communication', 'notification', 'storage', 'webhook'] as const
63
+
64
+ function categoryBadgeVariant(category: string | undefined): 'default' | 'secondary' | 'outline' {
65
+ if (!category) return 'outline'
66
+ return 'secondary'
67
+ }
68
+
69
+ export default function IntegrationsMarketplacePage() {
70
+ const [data, setData] = React.useState<ListResponse | null>(null)
71
+ const [isLoading, setIsLoading] = React.useState(true)
72
+ const [search, setSearch] = React.useState('')
73
+ const [category, setCategory] = React.useState<string>('all')
74
+ const [togglingIds, setTogglingIds] = React.useState<Set<string>>(new Set())
75
+ const scopeVersion = useOrganizationScopeVersion()
76
+ const t = useT()
77
+
78
+ const load = React.useCallback(async () => {
79
+ setIsLoading(true)
80
+ const fallback: ListResponse = { items: [], bundles: [] }
81
+ const call = await apiCall<ListResponse>('/api/integrations', undefined, { fallback })
82
+ if (!call.ok) {
83
+ flash(t('integrations.marketplace.loadError'), 'error')
84
+ setIsLoading(false)
85
+ return
86
+ }
87
+ setData(call.result ?? fallback)
88
+ setIsLoading(false)
89
+ }, [t])
90
+
91
+ React.useEffect(() => { void load() }, [load, scopeVersion])
92
+
93
+ const handleToggle = React.useCallback(async (integrationId: string, enabled: boolean) => {
94
+ setTogglingIds((prev) => new Set(prev).add(integrationId))
95
+ const call = await apiCall(`/api/integrations/${encodeURIComponent(integrationId)}/state`, {
96
+ method: 'PUT',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ isEnabled: enabled }),
99
+ }, { fallback: null })
100
+
101
+ if (!call.ok) {
102
+ flash(t('integrations.detail.stateError'), 'error')
103
+ } else {
104
+ setData((prev) => {
105
+ if (!prev) return prev
106
+ return {
107
+ ...prev,
108
+ items: prev.items.map((item) =>
109
+ item.id === integrationId ? { ...item, isEnabled: enabled } : item,
110
+ ),
111
+ }
112
+ })
113
+ }
114
+ setTogglingIds((prev) => { const next = new Set(prev); next.delete(integrationId); return next })
115
+ }, [t])
116
+
117
+ const filteredItems = React.useMemo(() => {
118
+ if (!data) return { bundles: [], standalone: [] }
119
+
120
+ let items = data.items
121
+ if (search) {
122
+ const q = search.toLowerCase()
123
+ items = items.filter((item) => item.title.toLowerCase().includes(q) || item.description?.toLowerCase().includes(q))
124
+ }
125
+ if (category !== 'all') {
126
+ items = items.filter((item) => item.category === category)
127
+ }
128
+
129
+ const bundled = new Map<string, IntegrationItem[]>()
130
+ const standalone: IntegrationItem[] = []
131
+ for (const item of items) {
132
+ if (item.bundleId) {
133
+ const list = bundled.get(item.bundleId) ?? []
134
+ list.push(item)
135
+ bundled.set(item.bundleId, list)
136
+ } else {
137
+ standalone.push(item)
138
+ }
139
+ }
140
+
141
+ const bundles = (data.bundles ?? [])
142
+ .filter((b) => bundled.has(b.id))
143
+ .map((b) => ({ ...b, integrations: bundled.get(b.id) ?? [] }))
144
+
145
+ return { bundles, standalone }
146
+ }, [data, search, category])
147
+
148
+ if (isLoading) {
149
+ return (
150
+ <Page>
151
+ <PageBody>
152
+ <div className="flex items-center justify-center py-16">
153
+ <Spinner />
154
+ </div>
155
+ </PageBody>
156
+ </Page>
157
+ )
158
+ }
159
+
160
+ return (
161
+ <Page>
162
+ <PageBody className="space-y-6">
163
+ <section className="space-y-6 rounded-lg border bg-background p-6">
164
+ <header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
165
+ <div className="space-y-1">
166
+ <h2 className="text-lg font-semibold">{t('integrations.marketplace.title')}</h2>
167
+ <p className="text-sm text-muted-foreground">
168
+ {t('integrations.marketplace.description')}
169
+ </p>
170
+ </div>
171
+ </header>
172
+
173
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
174
+ <Input
175
+ placeholder={t('integrations.marketplace.search')}
176
+ value={search}
177
+ onChange={(e) => setSearch(e.target.value)}
178
+ className="max-w-sm"
179
+ />
180
+ <div className="flex flex-wrap gap-1.5">
181
+ {CATEGORIES.map((cat) => {
182
+ const Icon = CATEGORY_ICONS[cat]
183
+ return (
184
+ <Button
185
+ key={cat}
186
+ type="button"
187
+ variant={category === cat ? 'default' : 'outline'}
188
+ size="sm"
189
+ onClick={() => setCategory(cat)}
190
+ >
191
+ {Icon ? <Icon className="mr-1.5 h-3.5 w-3.5" /> : null}
192
+ {t(`integrations.marketplace.categories.${cat}`)}
193
+ </Button>
194
+ )
195
+ })}
196
+ </div>
197
+ </div>
198
+
199
+ {filteredItems.bundles.map((bundle) => (
200
+ <Card key={bundle.id}>
201
+ <CardHeader>
202
+ <div className="flex items-center justify-between">
203
+ <div>
204
+ <CardTitle>{bundle.title}</CardTitle>
205
+ {bundle.description && (
206
+ <p className="text-muted-foreground text-sm mt-1">{bundle.description}</p>
207
+ )}
208
+ <p className="text-muted-foreground text-xs mt-1">
209
+ {t('integrations.marketplace.integrations', { count: bundle.integrations.length })}
210
+ </p>
211
+ </div>
212
+ <Button asChild variant="outline" size="sm">
213
+ <Link href={`/backend/integrations/bundle/${encodeURIComponent(bundle.id)}`}>
214
+ {t('integrations.marketplace.configure')}
215
+ </Link>
216
+ </Button>
217
+ </div>
218
+ </CardHeader>
219
+ <CardContent>
220
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
221
+ {bundle.integrations.map((item) => (
222
+ <div
223
+ key={item.id}
224
+ className="flex items-center justify-between rounded-lg border p-3"
225
+ >
226
+ <div className="min-w-0">
227
+ <Link
228
+ href={`/backend/integrations/${encodeURIComponent(item.id)}`}
229
+ className="text-sm font-medium hover:underline"
230
+ >
231
+ {item.title}
232
+ </Link>
233
+ {item.category && (
234
+ <Badge variant={categoryBadgeVariant(item.category)} className="ml-2 text-xs">
235
+ {item.category}
236
+ </Badge>
237
+ )}
238
+ </div>
239
+ <Switch
240
+ checked={item.isEnabled}
241
+ disabled={togglingIds.has(item.id)}
242
+ onCheckedChange={(checked) => void handleToggle(item.id, checked)}
243
+ />
244
+ </div>
245
+ ))}
246
+ </div>
247
+ </CardContent>
248
+ </Card>
249
+ ))}
250
+
251
+ {filteredItems.standalone.length > 0 && (
252
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
253
+ {filteredItems.standalone.map((item) => (
254
+ <Card key={item.id} className="flex flex-col">
255
+ <CardHeader>
256
+ <div className="flex items-center justify-between">
257
+ <CardTitle className="text-base">{item.title}</CardTitle>
258
+ <Switch
259
+ checked={item.isEnabled}
260
+ disabled={togglingIds.has(item.id)}
261
+ onCheckedChange={(checked) => void handleToggle(item.id, checked)}
262
+ />
263
+ </div>
264
+ {item.category && (
265
+ <Badge variant={categoryBadgeVariant(item.category)} className="w-fit text-xs">
266
+ {item.category}
267
+ </Badge>
268
+ )}
269
+ </CardHeader>
270
+ <CardContent className="flex-1">
271
+ {item.description && (
272
+ <p className="text-muted-foreground text-sm">{item.description}</p>
273
+ )}
274
+ </CardContent>
275
+ <div className="px-6 pb-4">
276
+ <Button asChild variant="outline" size="sm" className="w-full">
277
+ <Link href={`/backend/integrations/${encodeURIComponent(item.id)}`}>
278
+ {t('integrations.marketplace.configure')}
279
+ </Link>
280
+ </Button>
281
+ </div>
282
+ </Card>
283
+ ))}
284
+ </div>
285
+ )}
286
+
287
+ {filteredItems.bundles.length === 0 && filteredItems.standalone.length === 0 && (
288
+ <div className="text-center py-12 text-muted-foreground">
289
+ {t('integrations.marketplace.noResults')}
290
+ </div>
291
+ )}
292
+ </section>
293
+ </PageBody>
294
+ </Page>
295
+ )
296
+ }
@@ -8,11 +8,14 @@
8
8
  */
9
9
 
10
10
  import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
11
+ import type { EntityManager } from '@mikro-orm/postgresql'
12
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
13
  import type { ExternalIdEnrichment } from '@open-mercato/shared/modules/integrations/types'
12
14
  import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
13
15
  import { SyncExternalIdMapping } from './entities'
14
16
 
15
17
  type EntityRecord = Record<string, unknown> & { id: string }
18
+ type EnricherScope = EnricherContext & { targetEntity?: string; em: EntityManager }
16
19
 
17
20
  function buildIntegrationData(
18
21
  mappings: SyncExternalIdMapping[],
@@ -41,17 +44,24 @@ const externalIdMappingEnricher: ResponseEnricher<EntityRecord, ExternalIdEnrich
41
44
  critical: false,
42
45
  fallback: {},
43
46
 
44
- async enrichOne(record, context: EnricherContext & { targetEntity?: string }) {
45
- const em = (context.em as any).fork()
46
- const targetEntity = (context as any).targetEntity as string | undefined
47
+ async enrichOne(record, context: EnricherScope) {
48
+ const em = context.em.fork()
49
+ const targetEntity = context.targetEntity
47
50
  if (!targetEntity) return { ...record, _integrations: {} }
48
51
 
49
- const mappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {
50
- internalEntityType: targetEntity,
51
- internalEntityId: record.id,
52
- organizationId: context.organizationId,
53
- deletedAt: null,
54
- })
52
+ const mappings = await findWithDecryption(
53
+ em,
54
+ SyncExternalIdMapping,
55
+ {
56
+ internalEntityType: targetEntity,
57
+ internalEntityId: record.id,
58
+ organizationId: context.organizationId,
59
+ tenantId: context.tenantId,
60
+ deletedAt: null,
61
+ },
62
+ undefined,
63
+ { organizationId: context.organizationId, tenantId: context.tenantId },
64
+ )
55
65
 
56
66
  if (mappings.length === 0) return { ...record, _integrations: {} }
57
67
 
@@ -61,18 +71,25 @@ const externalIdMappingEnricher: ResponseEnricher<EntityRecord, ExternalIdEnrich
61
71
  }
62
72
  },
63
73
 
64
- async enrichMany(records, context: EnricherContext & { targetEntity?: string }) {
65
- const em = (context.em as any).fork()
66
- const targetEntity = (context as any).targetEntity as string | undefined
74
+ async enrichMany(records, context: EnricherScope) {
75
+ const em = context.em.fork()
76
+ const targetEntity = context.targetEntity
67
77
  if (!targetEntity || records.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))
68
78
 
69
79
  const recordIds = records.map((r) => r.id)
70
- const allMappings: SyncExternalIdMapping[] = await em.find(SyncExternalIdMapping, {
71
- internalEntityType: targetEntity,
72
- internalEntityId: { $in: recordIds },
73
- organizationId: context.organizationId,
74
- deletedAt: null,
75
- })
80
+ const allMappings = await findWithDecryption(
81
+ em,
82
+ SyncExternalIdMapping,
83
+ {
84
+ internalEntityType: targetEntity,
85
+ internalEntityId: { $in: recordIds },
86
+ organizationId: context.organizationId,
87
+ tenantId: context.tenantId,
88
+ deletedAt: null,
89
+ },
90
+ undefined,
91
+ { organizationId: context.organizationId, tenantId: context.tenantId },
92
+ )
76
93
 
77
94
  if (allMappings.length === 0) return records.map((r) => ({ ...r, _integrations: {} }))
78
95
 
@@ -1,13 +1,10 @@
1
- import { Entity, PrimaryKey, Property, Index } from '@mikro-orm/core'
1
+ import { Entity, PrimaryKey, Property, Index, OptionalProps } from '@mikro-orm/core'
2
2
 
3
- /**
4
- * Stores mappings between internal entity IDs and external system IDs.
5
- * Used by integration modules to track synced records across platforms.
6
- */
7
3
  @Entity({ tableName: 'sync_external_id_mappings' })
8
4
  @Index({ properties: ['internalEntityType', 'internalEntityId', 'organizationId'] })
9
5
  @Index({ properties: ['integrationId', 'externalId', 'organizationId'] })
10
6
  export class SyncExternalIdMapping {
7
+ [OptionalProps]?: 'syncStatus' | 'lastSyncedAt' | 'createdAt' | 'updatedAt' | 'deletedAt'
11
8
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
12
9
  id!: string
13
10
 
@@ -44,3 +41,115 @@ export class SyncExternalIdMapping {
44
41
  @Property({ name: 'deleted_at', type: Date, nullable: true })
45
42
  deletedAt?: Date | null
46
43
  }
44
+
45
+ @Entity({ tableName: 'integration_credentials' })
46
+ @Index({ properties: ['integrationId', 'organizationId', 'tenantId'] })
47
+ export class IntegrationCredentials {
48
+ [OptionalProps]?: 'createdAt' | 'updatedAt' | 'deletedAt'
49
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
50
+ id!: string
51
+
52
+ @Property({ name: 'integration_id', type: 'text' })
53
+ integrationId!: string
54
+
55
+ @Property({ name: 'credentials', type: 'json' })
56
+ credentials!: Record<string, unknown>
57
+
58
+ @Property({ name: 'organization_id', type: 'uuid' })
59
+ organizationId!: string
60
+
61
+ @Property({ name: 'tenant_id', type: 'uuid' })
62
+ tenantId!: string
63
+
64
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
65
+ createdAt: Date = new Date()
66
+
67
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
68
+ updatedAt: Date = new Date()
69
+
70
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
71
+ deletedAt?: Date | null
72
+ }
73
+
74
+ @Entity({ tableName: 'integration_states' })
75
+ @Index({ properties: ['integrationId', 'organizationId', 'tenantId'] })
76
+ export class IntegrationState {
77
+ [OptionalProps]?: 'isEnabled' | 'apiVersion' | 'reauthRequired' | 'lastHealthStatus' | 'lastHealthCheckedAt' | 'createdAt' | 'updatedAt' | 'deletedAt'
78
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
79
+ id!: string
80
+
81
+ @Property({ name: 'integration_id', type: 'text' })
82
+ integrationId!: string
83
+
84
+ @Property({ name: 'is_enabled', type: 'boolean', default: true })
85
+ isEnabled: boolean = true
86
+
87
+ @Property({ name: 'api_version', type: 'text', nullable: true })
88
+ apiVersion?: string | null
89
+
90
+ @Property({ name: 'reauth_required', type: 'boolean', default: false })
91
+ reauthRequired: boolean = false
92
+
93
+ @Property({ name: 'last_health_status', type: 'text', nullable: true })
94
+ lastHealthStatus?: 'healthy' | 'degraded' | 'unhealthy' | null
95
+
96
+ @Property({ name: 'last_health_checked_at', type: Date, nullable: true })
97
+ lastHealthCheckedAt?: Date | null
98
+
99
+ @Property({ name: 'organization_id', type: 'uuid' })
100
+ organizationId!: string
101
+
102
+ @Property({ name: 'tenant_id', type: 'uuid' })
103
+ tenantId!: string
104
+
105
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
106
+ createdAt: Date = new Date()
107
+
108
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
109
+ updatedAt: Date = new Date()
110
+
111
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
112
+ deletedAt?: Date | null
113
+ }
114
+
115
+ @Entity({ tableName: 'integration_logs' })
116
+ @Index({ properties: ['integrationId', 'organizationId', 'tenantId', 'createdAt'] })
117
+ @Index({ properties: ['level', 'organizationId', 'tenantId', 'createdAt'] })
118
+ export class IntegrationLog {
119
+ [OptionalProps]?: 'runId' | 'scopeEntityType' | 'scopeEntityId' | 'code' | 'payload' | 'createdAt'
120
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
121
+ id!: string
122
+
123
+ @Property({ name: 'integration_id', type: 'text' })
124
+ integrationId!: string
125
+
126
+ @Property({ name: 'run_id', type: 'uuid', nullable: true })
127
+ runId?: string | null
128
+
129
+ @Property({ name: 'scope_entity_type', type: 'text', nullable: true })
130
+ scopeEntityType?: string | null
131
+
132
+ @Property({ name: 'scope_entity_id', type: 'uuid', nullable: true })
133
+ scopeEntityId?: string | null
134
+
135
+ @Property({ name: 'level', type: 'text' })
136
+ level!: 'info' | 'warn' | 'error'
137
+
138
+ @Property({ name: 'message', type: 'text' })
139
+ message!: string
140
+
141
+ @Property({ name: 'code', type: 'text', nullable: true })
142
+ code?: string | null
143
+
144
+ @Property({ name: 'payload', type: 'json', nullable: true })
145
+ payload?: Record<string, unknown> | null
146
+
147
+ @Property({ name: 'organization_id', type: 'uuid' })
148
+ organizationId!: string
149
+
150
+ @Property({ name: 'tenant_id', type: 'uuid' })
151
+ tenantId!: string
152
+
153
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
154
+ createdAt: Date = new Date()
155
+ }
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod'
2
+
3
+ export const saveCredentialsSchema = z.object({
4
+ credentials: z.record(
5
+ z.string().min(1).max(128),
6
+ z.union([z.string().max(20_000), z.number(), z.boolean(), z.null()]),
7
+ ),
8
+ }).refine((value) => Object.keys(value.credentials).length <= 200, {
9
+ message: 'At most 200 credential fields are allowed',
10
+ })
11
+
12
+ export type SaveCredentialsInput = z.infer<typeof saveCredentialsSchema>
13
+
14
+ export const updateVersionSchema = z.object({
15
+ apiVersion: z.string().min(1),
16
+ })
17
+
18
+ export type UpdateVersionInput = z.infer<typeof updateVersionSchema>
19
+
20
+ export const updateStateSchema = z.object({
21
+ isEnabled: z.boolean().optional(),
22
+ reauthRequired: z.boolean().optional(),
23
+ }).refine((value) => value.isEnabled !== undefined || value.reauthRequired !== undefined, {
24
+ message: 'At least one state field must be provided',
25
+ })
26
+
27
+ export type UpdateStateInput = z.infer<typeof updateStateSchema>
28
+
29
+ export const integrationLogLevelSchema = z.enum(['info', 'warn', 'error'])
30
+
31
+ export const listIntegrationLogsQuerySchema = z.object({
32
+ integrationId: z.string().min(1).optional(),
33
+ level: integrationLogLevelSchema.optional(),
34
+ runId: z.string().uuid().optional(),
35
+ entityType: z.string().optional(),
36
+ entityId: z.string().uuid().optional(),
37
+ page: z.coerce.number().int().min(1).default(1),
38
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
39
+ })
40
+
41
+ export type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>
@@ -0,0 +1,31 @@
1
+ import { asFunction, asValue } from 'awilix'
2
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { IntegrationCredentials, IntegrationLog, IntegrationState, SyncExternalIdMapping } from './data/entities'
5
+ import { createCredentialsService } from './lib/credentials-service'
6
+ import { createIntegrationStateService } from './lib/state-service'
7
+ import { createIntegrationLogService } from './lib/log-service'
8
+ import { createHealthService } from './lib/health-service'
9
+ import type { IntegrationStateService } from './lib/state-service'
10
+ import type { IntegrationLogService } from './lib/log-service'
11
+
12
+ type Cradle = {
13
+ em: EntityManager
14
+ integrationStateService: IntegrationStateService
15
+ integrationLogService: IntegrationLogService
16
+ }
17
+
18
+ export function register(container: AppContainer) {
19
+ container.register({
20
+ integrationCredentialsService: asFunction(({ em }: Cradle) => createCredentialsService(em)).scoped().proxy(),
21
+ integrationStateService: asFunction(({ em }: Cradle) => createIntegrationStateService(em)).scoped().proxy(),
22
+ integrationLogService: asFunction(({ em }: Cradle) => createIntegrationLogService(em)).scoped().proxy(),
23
+ integrationHealthService: asFunction(({ integrationStateService, integrationLogService }: Cradle) =>
24
+ createHealthService(container, integrationStateService, integrationLogService),
25
+ ).scoped().proxy(),
26
+ SyncExternalIdMapping: asValue(SyncExternalIdMapping),
27
+ IntegrationCredentials: asValue(IntegrationCredentials),
28
+ IntegrationState: asValue(IntegrationState),
29
+ IntegrationLog: asValue(IntegrationLog),
30
+ })
31
+ }
@@ -0,0 +1,17 @@
1
+ import { createModuleEvents } from '@open-mercato/shared/modules/events'
2
+
3
+ const events = [
4
+ { id: 'integrations.credentials.updated', label: 'Integration Credentials Updated', category: 'custom', entity: 'credentials' },
5
+ { id: 'integrations.state.updated', label: 'Integration State Updated', category: 'custom', entity: 'state' },
6
+ { id: 'integrations.version.changed', label: 'Integration Version Changed', category: 'custom', entity: 'state' },
7
+ { id: 'integrations.log.created', label: 'Integration Log Created', category: 'system', entity: 'log', excludeFromTriggers: true },
8
+ ] as const
9
+
10
+ export const eventsConfig = createModuleEvents({
11
+ moduleId: 'integrations',
12
+ events,
13
+ })
14
+
15
+ export const emitIntegrationsEvent = eventsConfig.emit
16
+
17
+ export default eventsConfig