@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,121 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
6
+ import { emitIntegrationsEvent } from '../../../events'
7
+ import { updateStateSchema } from '../../../data/validators'
8
+ import type { IntegrationStateService } from '../../../lib/state-service'
9
+ import {
10
+ resolveUserFeatures,
11
+ runIntegrationMutationGuardAfterSuccess,
12
+ runIntegrationMutationGuards,
13
+ } from '../../guards'
14
+
15
+ const idParamsSchema = z.object({ id: z.string().min(1) })
16
+
17
+ export const metadata = {
18
+ PUT: { requireAuth: true, requireFeatures: ['integrations.manage'] },
19
+ }
20
+
21
+ export const openApi = {
22
+ tags: ['Integrations'],
23
+ summary: 'Update integration state',
24
+ }
25
+
26
+ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
27
+ const auth = await getAuthFromRequest(req)
28
+ if (!auth?.tenantId || !auth.orgId) {
29
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
30
+ }
31
+
32
+ const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
33
+ ? await (ctx.params as Promise<{ id?: string }>)
34
+ : (ctx.params as { id?: string } | undefined)
35
+
36
+ const parsedParams = idParamsSchema.safeParse(rawParams)
37
+ if (!parsedParams.success) {
38
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
39
+ }
40
+
41
+ const integration = getIntegration(parsedParams.data.id)
42
+ if (!integration) {
43
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
44
+ }
45
+
46
+ const payload = await req.json().catch(() => null)
47
+ const parsedBody = updateStateSchema.safeParse(payload)
48
+ if (!parsedBody.success) {
49
+ return NextResponse.json({ error: 'Invalid state payload', details: parsedBody.error.flatten() }, { status: 422 })
50
+ }
51
+
52
+ const container = await createRequestContainer()
53
+ const guardResult = await runIntegrationMutationGuards(
54
+ container,
55
+ {
56
+ tenantId: auth.tenantId,
57
+ organizationId: auth.orgId,
58
+ userId: auth.sub ?? '',
59
+ resourceKind: 'integrations.integration',
60
+ resourceId: integration.id,
61
+ operation: 'update',
62
+ requestMethod: req.method,
63
+ requestHeaders: req.headers,
64
+ mutationPayload: parsedBody.data as Record<string, unknown>,
65
+ },
66
+ resolveUserFeatures(auth),
67
+ )
68
+ if (!guardResult.ok) {
69
+ return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
70
+ }
71
+
72
+ let payloadData = parsedBody.data
73
+ if (guardResult.modifiedPayload) {
74
+ const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }
75
+ const reparsed = updateStateSchema.safeParse(mergedPayload)
76
+ if (!reparsed.success) {
77
+ return NextResponse.json({ error: 'Invalid state payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })
78
+ }
79
+ payloadData = reparsed.data
80
+ }
81
+
82
+ const stateService = container.resolve('integrationStateService') as IntegrationStateService
83
+
84
+ const state = await stateService.upsert(
85
+ integration.id,
86
+ {
87
+ isEnabled: payloadData.isEnabled,
88
+ reauthRequired: payloadData.reauthRequired,
89
+ },
90
+ {
91
+ organizationId: auth.orgId as string,
92
+ tenantId: auth.tenantId,
93
+ },
94
+ )
95
+
96
+ await emitIntegrationsEvent('integrations.state.updated', {
97
+ integrationId: integration.id,
98
+ isEnabled: state.isEnabled,
99
+ reauthRequired: state.reauthRequired,
100
+ tenantId: auth.tenantId,
101
+ organizationId: auth.orgId,
102
+ userId: auth.sub,
103
+ })
104
+
105
+ await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
106
+ tenantId: auth.tenantId,
107
+ organizationId: auth.orgId,
108
+ userId: auth.sub ?? '',
109
+ resourceKind: 'integrations.integration',
110
+ resourceId: integration.id,
111
+ operation: 'update',
112
+ requestMethod: req.method,
113
+ requestHeaders: req.headers,
114
+ })
115
+
116
+ return NextResponse.json({
117
+ isEnabled: state.isEnabled,
118
+ reauthRequired: state.reauthRequired,
119
+ apiVersion: state.apiVersion ?? null,
120
+ })
121
+ }
@@ -0,0 +1,132 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
6
+ import { emitIntegrationsEvent } from '../../../events'
7
+ import { updateVersionSchema } from '../../../data/validators'
8
+ import type { IntegrationStateService } from '../../../lib/state-service'
9
+ import { resolveDefaultApiVersion } from '../../../lib/registry-service'
10
+ import {
11
+ resolveUserFeatures,
12
+ runIntegrationMutationGuardAfterSuccess,
13
+ runIntegrationMutationGuards,
14
+ } from '../../guards'
15
+
16
+ const idParamsSchema = z.object({ id: z.string().min(1) })
17
+
18
+ export const metadata = {
19
+ PUT: { requireAuth: true, requireFeatures: ['integrations.manage'] },
20
+ }
21
+
22
+ export const openApi = {
23
+ tags: ['Integrations'],
24
+ summary: 'Change integration API version',
25
+ }
26
+
27
+ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {
28
+ const auth = await getAuthFromRequest(req)
29
+ if (!auth?.tenantId || !auth.orgId) {
30
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
31
+ }
32
+
33
+ const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')
34
+ ? await (ctx.params as Promise<{ id?: string }>)
35
+ : (ctx.params as { id?: string } | undefined)
36
+
37
+ const parsedParams = idParamsSchema.safeParse(rawParams)
38
+ if (!parsedParams.success) {
39
+ return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })
40
+ }
41
+
42
+ const integration = getIntegration(parsedParams.data.id)
43
+ if (!integration) {
44
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
45
+ }
46
+
47
+ const payload = await req.json().catch(() => null)
48
+ const parsedBody = updateVersionSchema.safeParse(payload)
49
+ if (!parsedBody.success) {
50
+ return NextResponse.json({ error: 'Invalid payload', details: parsedBody.error.flatten() }, { status: 422 })
51
+ }
52
+
53
+ const requestedVersion = parsedBody.data.apiVersion
54
+ const availableVersions = integration.apiVersions ?? []
55
+ if (availableVersions.length === 0) {
56
+ return NextResponse.json({ error: 'This integration is not versioned' }, { status: 422 })
57
+ }
58
+
59
+ const exists = availableVersions.some((version) => version.id === requestedVersion)
60
+ if (!exists) {
61
+ return NextResponse.json({ error: 'Unknown integration version' }, { status: 422 })
62
+ }
63
+
64
+ const defaultVersion = resolveDefaultApiVersion(availableVersions)
65
+ if (!defaultVersion) {
66
+ return NextResponse.json({ error: 'Integration version configuration is invalid' }, { status: 422 })
67
+ }
68
+
69
+ const container = await createRequestContainer()
70
+ const guardResult = await runIntegrationMutationGuards(
71
+ container,
72
+ {
73
+ tenantId: auth.tenantId,
74
+ organizationId: auth.orgId,
75
+ userId: auth.sub ?? '',
76
+ resourceKind: 'integrations.integration',
77
+ resourceId: integration.id,
78
+ operation: 'update',
79
+ requestMethod: req.method,
80
+ requestHeaders: req.headers,
81
+ mutationPayload: parsedBody.data as Record<string, unknown>,
82
+ },
83
+ resolveUserFeatures(auth),
84
+ )
85
+ if (!guardResult.ok) {
86
+ return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
87
+ }
88
+
89
+ let payloadData = parsedBody.data
90
+ if (guardResult.modifiedPayload) {
91
+ const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }
92
+ const reparsed = updateVersionSchema.safeParse(mergedPayload)
93
+ if (!reparsed.success) {
94
+ return NextResponse.json({ error: 'Invalid payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })
95
+ }
96
+ payloadData = reparsed.data
97
+ }
98
+ if (!availableVersions.some((version) => version.id === payloadData.apiVersion)) {
99
+ return NextResponse.json({ error: 'Unknown integration version' }, { status: 422 })
100
+ }
101
+
102
+ const stateService = container.resolve('integrationStateService') as IntegrationStateService
103
+ const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
104
+
105
+ const before = await stateService.resolveApiVersion(integration.id, scope)
106
+ await stateService.upsert(integration.id, { apiVersion: payloadData.apiVersion }, scope)
107
+
108
+ await emitIntegrationsEvent('integrations.version.changed', {
109
+ integrationId: integration.id,
110
+ previousVersion: before ?? defaultVersion,
111
+ apiVersion: payloadData.apiVersion,
112
+ tenantId: auth.tenantId,
113
+ organizationId: auth.orgId,
114
+ userId: auth.sub,
115
+ })
116
+
117
+ await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
118
+ tenantId: auth.tenantId,
119
+ organizationId: auth.orgId,
120
+ userId: auth.sub ?? '',
121
+ resourceKind: 'integrations.integration',
122
+ resourceId: integration.id,
123
+ operation: 'update',
124
+ requestMethod: req.method,
125
+ requestHeaders: req.headers,
126
+ })
127
+
128
+ return NextResponse.json({
129
+ apiVersion: payloadData.apiVersion,
130
+ previousVersion: before ?? defaultVersion,
131
+ })
132
+ }
@@ -0,0 +1,59 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import {
3
+ bridgeLegacyGuard,
4
+ runMutationGuards,
5
+ type MutationGuard,
6
+ type MutationGuardInput,
7
+ } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
8
+
9
+ type GuardAfterCallback = {
10
+ guard: MutationGuard
11
+ metadata: Record<string, unknown> | null
12
+ }
13
+
14
+ export function resolveUserFeatures(auth: unknown): string[] {
15
+ const features = (auth as { features?: unknown })?.features
16
+ if (!Array.isArray(features)) return []
17
+ return features.filter((value): value is string => typeof value === 'string')
18
+ }
19
+
20
+ export async function runIntegrationMutationGuards(
21
+ container: AwilixContainer,
22
+ input: MutationGuardInput,
23
+ userFeatures: string[],
24
+ ): Promise<{
25
+ ok: boolean
26
+ errorBody?: Record<string, unknown>
27
+ errorStatus?: number
28
+ modifiedPayload?: Record<string, unknown>
29
+ afterSuccessCallbacks: GuardAfterCallback[]
30
+ }> {
31
+ const legacyGuard = bridgeLegacyGuard(container)
32
+ if (!legacyGuard) {
33
+ return { ok: true, afterSuccessCallbacks: [] }
34
+ }
35
+
36
+ return runMutationGuards([legacyGuard], input, { userFeatures })
37
+ }
38
+
39
+ export async function runIntegrationMutationGuardAfterSuccess(
40
+ callbacks: GuardAfterCallback[],
41
+ input: {
42
+ tenantId: string
43
+ organizationId: string | null
44
+ userId: string
45
+ resourceKind: string
46
+ resourceId: string
47
+ operation: 'create' | 'update' | 'delete'
48
+ requestMethod: string
49
+ requestHeaders: Headers
50
+ },
51
+ ): Promise<void> {
52
+ for (const callback of callbacks) {
53
+ if (!callback.guard.afterSuccess) continue
54
+ await callback.guard.afterSuccess({
55
+ ...input,
56
+ metadata: callback.metadata ?? null,
57
+ })
58
+ }
59
+ }
@@ -0,0 +1,63 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { listIntegrationLogsQuerySchema } from '../../data/validators'
5
+ import type { IntegrationLogService } from '../../lib/log-service'
6
+
7
+ export const metadata = {
8
+ GET: { requireAuth: true, requireFeatures: ['integrations.view'] },
9
+ }
10
+
11
+ export const openApi = {
12
+ tags: ['Integrations'],
13
+ summary: 'List integration logs',
14
+ }
15
+
16
+ export async function GET(req: Request) {
17
+ const auth = await getAuthFromRequest(req)
18
+ if (!auth?.tenantId || !auth.orgId) {
19
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
20
+ }
21
+
22
+ const url = new URL(req.url)
23
+ const parsed = listIntegrationLogsQuerySchema.safeParse({
24
+ integrationId: url.searchParams.get('integrationId') ?? undefined,
25
+ level: url.searchParams.get('level') ?? undefined,
26
+ runId: url.searchParams.get('runId') ?? undefined,
27
+ entityType: url.searchParams.get('entityType') ?? undefined,
28
+ entityId: url.searchParams.get('entityId') ?? undefined,
29
+ page: url.searchParams.get('page') ?? undefined,
30
+ pageSize: url.searchParams.get('pageSize') ?? undefined,
31
+ })
32
+
33
+ if (!parsed.success) {
34
+ return NextResponse.json({ error: 'Invalid query', details: parsed.error.flatten() }, { status: 400 })
35
+ }
36
+
37
+ const container = await createRequestContainer()
38
+ const logService = container.resolve('integrationLogService') as IntegrationLogService
39
+
40
+ const { items, total } = await logService.query(parsed.data, {
41
+ organizationId: auth.orgId as string,
42
+ tenantId: auth.tenantId,
43
+ })
44
+
45
+ return NextResponse.json({
46
+ items: items.map((item) => ({
47
+ id: item.id,
48
+ integrationId: item.integrationId,
49
+ runId: item.runId ?? null,
50
+ scopeEntityType: item.scopeEntityType ?? null,
51
+ scopeEntityId: item.scopeEntityId ?? null,
52
+ level: item.level,
53
+ message: item.message,
54
+ code: item.code ?? null,
55
+ payload: item.payload ?? null,
56
+ createdAt: item.createdAt.toISOString(),
57
+ })),
58
+ total,
59
+ page: parsed.data.page,
60
+ pageSize: parsed.data.pageSize,
61
+ totalPages: Math.max(1, Math.ceil(total / parsed.data.pageSize)),
62
+ })
63
+ }
@@ -0,0 +1,22 @@
1
+ import { z, type ZodTypeAny } from 'zod'
2
+ import { createCrudOpenApiFactory, createPagedListResponseSchema as createSharedPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'
3
+
4
+ export function createPagedListResponseSchema(itemSchema: ZodTypeAny) {
5
+ return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })
6
+ }
7
+
8
+ export const integrationInfoSchema = z.object({
9
+ id: z.string(),
10
+ title: z.string(),
11
+ category: z.string().nullable(),
12
+ hub: z.string().nullable(),
13
+ providerKey: z.string().nullable(),
14
+ bundleId: z.string().nullable(),
15
+ hasCredentials: z.boolean(),
16
+ isEnabled: z.boolean(),
17
+ apiVersion: z.string().nullable(),
18
+ })
19
+
20
+ export const buildIntegrationsCrudOpenApi = createCrudOpenApiFactory({
21
+ defaultTag: 'Integrations',
22
+ })
@@ -0,0 +1,73 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import type { CredentialsService } from '../lib/credentials-service'
5
+ import type { IntegrationStateService } from '../lib/state-service'
6
+ import { getAllBundles, getAllIntegrations } from '@open-mercato/shared/modules/integrations/types'
7
+ import { buildIntegrationsCrudOpenApi, createPagedListResponseSchema, integrationInfoSchema } from './openapi'
8
+
9
+ export const metadata = {
10
+ GET: { requireAuth: true, requireFeatures: ['integrations.view'] },
11
+ }
12
+
13
+ export const openApi = buildIntegrationsCrudOpenApi({
14
+ resourceName: 'Integration',
15
+ pluralName: 'Integrations',
16
+ listResponseSchema: createPagedListResponseSchema(integrationInfoSchema),
17
+ querySchema: undefined,
18
+ })
19
+
20
+ export async function GET(req: Request) {
21
+ const auth = await getAuthFromRequest(req)
22
+ if (!auth?.tenantId || !auth.orgId) {
23
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
24
+ }
25
+
26
+ const container = await createRequestContainer()
27
+ const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
28
+ const stateService = container.resolve('integrationStateService') as IntegrationStateService
29
+
30
+ const rows = await Promise.all(
31
+ getAllIntegrations().map(async (integration) => {
32
+ const [resolvedCredentials, state] = await Promise.all([
33
+ credentialsService.resolve(integration.id, { organizationId: auth.orgId as string, tenantId: auth.tenantId as string }),
34
+ stateService.get(integration.id, { organizationId: auth.orgId as string, tenantId: auth.tenantId as string }),
35
+ ])
36
+
37
+ return {
38
+ id: integration.id,
39
+ title: integration.title,
40
+ category: integration.category ?? null,
41
+ hub: integration.hub ?? null,
42
+ providerKey: integration.providerKey ?? null,
43
+ bundleId: integration.bundleId ?? null,
44
+ hasCredentials: Boolean(resolvedCredentials),
45
+ isEnabled: state?.isEnabled ?? true,
46
+ apiVersion: state?.apiVersion ?? null,
47
+ }
48
+ }),
49
+ )
50
+
51
+ const bundles = getAllBundles().map((bundle) => {
52
+ const bundleIntegrations = rows.filter((row) => row.bundleId === bundle.id)
53
+ const enabledCount = bundleIntegrations.reduce((count, integration) => count + (integration.isEnabled ? 1 : 0), 0)
54
+
55
+ return {
56
+ id: bundle.id,
57
+ title: bundle.title,
58
+ description: bundle.description,
59
+ icon: bundle.icon ?? null,
60
+ integrationCount: bundleIntegrations.length,
61
+ enabledCount,
62
+ }
63
+ })
64
+
65
+ return NextResponse.json({
66
+ items: rows,
67
+ bundles,
68
+ total: rows.length,
69
+ page: 1,
70
+ pageSize: 100,
71
+ totalPages: 1,
72
+ })
73
+ }
@@ -0,0 +1,11 @@
1
+ export const metadata = {
2
+ requireAuth: true,
3
+ requireFeatures: ['integrations.view'],
4
+ pageContext: 'settings' as const,
5
+ pageTitle: 'Integration Detail',
6
+ pageTitleKey: 'integrations.detail.title',
7
+ navHidden: true,
8
+ breadcrumb: [
9
+ { label: 'Integrations', labelKey: 'integrations.nav.title', href: '/backend/integrations' },
10
+ ],
11
+ }