@friggframework/core 2.0.0--canary.703.12b980b.0 → 2.0.0--canary.707.df80bfd.0

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 (308) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +20 -10
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -2
  19. package/database/encryption/README.md +78 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +121 -24
  23. package/database/encryption/field-encryption-service.js +69 -5
  24. package/database/encryption/integration-mapping-encryption.js +1 -1
  25. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  26. package/database/organization-where.js +18 -0
  27. package/database/prisma.js +15 -38
  28. package/database/unique-violation.js +25 -0
  29. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  30. package/errors/index.js +0 -4
  31. package/extensions/bootstrap-runner.js +43 -0
  32. package/extensions/extension-loader.js +230 -0
  33. package/extensions/index.js +44 -0
  34. package/extensions/initialize-app.js +123 -0
  35. package/extensions/prisma-composer.js +270 -0
  36. package/extensions/route-mounter.js +103 -0
  37. package/extensions/schema-composer.js +188 -0
  38. package/generated/prisma-mongodb/edge.js +33 -42
  39. package/generated/prisma-mongodb/index-browser.js +30 -39
  40. package/generated/prisma-mongodb/index.d.ts +3187 -2005
  41. package/generated/prisma-mongodb/index.js +33 -42
  42. package/generated/prisma-mongodb/package.json +1 -1
  43. package/generated/prisma-mongodb/schema.prisma +86 -59
  44. package/generated/prisma-mongodb/wasm.js +33 -42
  45. package/generated/prisma-postgresql/edge.js +33 -42
  46. package/generated/prisma-postgresql/index-browser.js +30 -39
  47. package/generated/prisma-postgresql/index.d.ts +3380 -2177
  48. package/generated/prisma-postgresql/index.js +33 -42
  49. package/generated/prisma-postgresql/package.json +1 -1
  50. package/generated/prisma-postgresql/schema.prisma +81 -57
  51. package/generated/prisma-postgresql/wasm.js +33 -42
  52. package/handlers/WEBHOOKS.md +0 -29
  53. package/handlers/app-definition-loader.js +128 -19
  54. package/handlers/app-handler-helpers.js +27 -8
  55. package/handlers/backend-utils.js +31 -25
  56. package/handlers/integration-event-dispatcher.js +3 -7
  57. package/handlers/middleware/admin-auth.js +14 -1
  58. package/handlers/routers/auth.js +46 -10
  59. package/handlers/routers/health.js +13 -4
  60. package/handlers/routers/integration-defined-routers.js +144 -92
  61. package/handlers/routers/integration-webhook-routers.js +100 -50
  62. package/handlers/workers/integration-defined-workers.js +5 -2
  63. package/index.js +66 -16
  64. package/infrastructure/scheduler/index.js +19 -6
  65. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  66. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  67. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  68. package/integrations/index.js +1 -14
  69. package/integrations/integration-base.js +2 -163
  70. package/integrations/integration-router.js +132 -102
  71. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  72. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  73. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  74. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  75. package/integrations/repositories/integration-mapping-repository.js +0 -29
  76. package/integrations/repositories/integration-not-found.js +14 -0
  77. package/integrations/repositories/integration-repository-documentdb.js +37 -9
  78. package/integrations/repositories/integration-repository-interface.js +17 -4
  79. package/integrations/repositories/integration-repository-mongo.js +64 -96
  80. package/integrations/repositories/integration-repository-postgres.js +63 -81
  81. package/integrations/repositories/process-repository-mongo.js +1 -1
  82. package/integrations/schedule-config.js +80 -0
  83. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  84. package/integrations/use-cases/create-integration.js +56 -31
  85. package/integrations/use-cases/delete-integration-for-user.js +4 -4
  86. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  87. package/integrations/use-cases/get-integration-for-user.js +2 -4
  88. package/integrations/use-cases/get-integration-instance.js +4 -4
  89. package/integrations/use-cases/get-integrations-for-user.js +79 -39
  90. package/integrations/use-cases/get-possible-integrations.js +2 -7
  91. package/integrations/use-cases/index.js +7 -1
  92. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  93. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  94. package/integrations/use-cases/update-integration.js +4 -4
  95. package/integrations/utils/map-integration-dto.js +43 -14
  96. package/management-api/create-management-api-router.js +49 -0
  97. package/management-api/error-response.js +116 -0
  98. package/management-api/management-api-config.js +151 -0
  99. package/management-api/meta-router.js +76 -0
  100. package/management-api/openapi/build-openapi.js +144 -0
  101. package/management-api/openapi/components.js +415 -0
  102. package/management-api/openapi/v1-operations.js +83 -0
  103. package/management-api/openapi/v2-operations.js +264 -0
  104. package/management-api/route-registry.js +411 -0
  105. package/management-api/test-support/http-client.js +39 -0
  106. package/management-api/use-cases/get-management-api-meta.js +57 -0
  107. package/management-api/v1-deprecation.js +120 -0
  108. package/management-api/v2/create-v2-router.js +85 -0
  109. package/management-api/v2/dependencies.js +281 -0
  110. package/management-api/v2/dto.js +143 -0
  111. package/management-api/v2/handlers/authorization.js +94 -0
  112. package/management-api/v2/handlers/credentials.js +33 -0
  113. package/management-api/v2/handlers/entities.js +77 -0
  114. package/management-api/v2/handlers/integrations.js +128 -0
  115. package/management-api/v2/handlers/proxy.js +34 -0
  116. package/management-api/v2/request.js +85 -0
  117. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  118. package/modules/domain/authorization-session.js +144 -0
  119. package/modules/entity-scope.js +15 -0
  120. package/modules/entity-type-registry.js +311 -0
  121. package/modules/index.js +14 -0
  122. package/modules/module-factory.js +12 -0
  123. package/modules/module.js +53 -2
  124. package/modules/proxy/proxy-bases.js +169 -0
  125. package/modules/proxy/proxy-errors.js +31 -0
  126. package/modules/proxy/proxy-path.js +183 -0
  127. package/modules/proxy/proxy-policy.js +277 -0
  128. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  129. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  130. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  131. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  132. package/modules/repositories/entity-not-found.js +18 -0
  133. package/modules/repositories/module-repository-documentdb.js +51 -6
  134. package/modules/repositories/module-repository-interface.js +19 -2
  135. package/modules/repositories/module-repository-mongo.js +67 -74
  136. package/modules/repositories/module-repository-postgres.js +66 -74
  137. package/modules/repositories/module-repository.js +2 -1
  138. package/modules/requester/proxy-request.js +245 -0
  139. package/modules/requester/requester.js +35 -5
  140. package/modules/scope-config.js +192 -0
  141. package/modules/scope-policy.js +112 -0
  142. package/modules/scope-resolution.js +288 -0
  143. package/modules/scope-visibility.js +247 -0
  144. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  145. package/modules/tests/doubles/ownership-matching.js +18 -0
  146. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  147. package/modules/use-cases/create-entity-from-credential.js +113 -0
  148. package/modules/use-cases/entity-operations-for-user.js +162 -0
  149. package/modules/use-cases/entity-types.js +139 -0
  150. package/modules/use-cases/execute-entity-proxy-request.js +331 -0
  151. package/modules/use-cases/get-authorization-requirements.js +100 -0
  152. package/modules/use-cases/get-entities-for-user.js +26 -13
  153. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  154. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  155. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  156. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  157. package/modules/use-cases/get-module.js +10 -4
  158. package/modules/use-cases/manage-access.js +21 -0
  159. package/modules/use-cases/multi-step-authorization.js +329 -0
  160. package/modules/use-cases/process-authorization-callback.js +53 -19
  161. package/modules/use-cases/refresh-entity-options.js +10 -4
  162. package/modules/use-cases/test-module-auth.js +10 -4
  163. package/package.json +6 -7
  164. package/prisma-mongodb/schema.prisma +86 -59
  165. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  166. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  167. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  168. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  169. package/prisma-postgresql/schema.prisma +81 -57
  170. package/providers/index.js +23 -0
  171. package/providers/resolve-provider.js +93 -0
  172. package/providers/runtime-detection.js +124 -0
  173. package/queues/index.js +25 -0
  174. package/queues/providers/index.js +11 -0
  175. package/queues/providers/netlify-background-provider.js +150 -0
  176. package/queues/providers/qstash-queue-provider.js +219 -0
  177. package/queues/providers/sqs-queue-provider.js +70 -0
  178. package/queues/queue-client-interface.js +55 -0
  179. package/queues/queue-message-auth.js +193 -0
  180. package/queues/queue-provider-factory.js +101 -0
  181. package/queues/queue-provider.js +65 -0
  182. package/queues/queuer-util.js +2 -9
  183. package/queues/sqs-queue-client.js +81 -0
  184. package/types/index.d.ts +2 -0
  185. package/types/modules/index.d.ts +214 -0
  186. package/types/user/index.d.ts +134 -0
  187. package/user/adopter-jwt-config.js +177 -0
  188. package/user/adopter-jwt-errors.js +59 -0
  189. package/user/adopter-token-verifier.js +122 -0
  190. package/user/app-user-helpers.js +97 -0
  191. package/user/create-user-authentication.js +64 -0
  192. package/user/membership.js +51 -0
  193. package/user/repositories/organization-membership-record.js +19 -0
  194. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  195. package/user/repositories/organization-membership-repository-factory.js +42 -0
  196. package/user/repositories/organization-membership-repository-interface.js +57 -0
  197. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  198. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  199. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  200. package/user/repositories/user-repository-documentdb.js +27 -2
  201. package/user/repositories/user-repository-interface.js +18 -0
  202. package/user/repositories/user-repository-mongo.js +24 -0
  203. package/user/repositories/user-repository-postgres.js +18 -0
  204. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  205. package/user/use-cases/authenticate-user.js +7 -1
  206. package/user/use-cases/ensure-organization-membership.js +92 -0
  207. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  208. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  209. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  210. package/user/user.js +64 -1
  211. package/errors/mapping-already-exists-error.js +0 -14
  212. package/handlers/workers/integration-triggers.js +0 -25
  213. package/infrastructure/scheduler/recurrence.js +0 -204
  214. package/integrations/define/DESIGN.md +0 -122
  215. package/integrations/define/config.js +0 -98
  216. package/integrations/define/define-app.js +0 -66
  217. package/integrations/define/define-extension.js +0 -154
  218. package/integrations/define/define-integration.js +0 -335
  219. package/integrations/define/examples/deal-alerts.js +0 -140
  220. package/integrations/define/extension-binding.js +0 -174
  221. package/integrations/define/index.js +0 -11
  222. package/integrations/define/json-schema-lite.js +0 -68
  223. package/integrations/define/subscriptions.js +0 -186
  224. package/integrations/define/tests/fixtures/hubspot.js +0 -109
  225. package/integrations/define/tests/fixtures/slack.js +0 -26
  226. package/integrations/define/use.js +0 -39
  227. package/integrations/definitions/README.md +0 -278
  228. package/integrations/definitions/canonical.js +0 -11
  229. package/integrations/definitions/capabilities/AUDIT.md +0 -480
  230. package/integrations/definitions/capabilities/acmeerp.capabilities.json +0 -633
  231. package/integrations/definitions/capabilities/asana.capabilities.json +0 -279
  232. package/integrations/definitions/capabilities/calendly.capabilities.json +0 -385
  233. package/integrations/definitions/capabilities/docusign.capabilities.json +0 -314
  234. package/integrations/definitions/capabilities/fathom.capabilities.json +0 -238
  235. package/integrations/definitions/capabilities/github.capabilities.json +0 -585
  236. package/integrations/definitions/capabilities/hubspot.capabilities.json +0 -822
  237. package/integrations/definitions/capabilities/jira.capabilities.json +0 -391
  238. package/integrations/definitions/capabilities/klaviyo.capabilities.json +0 -503
  239. package/integrations/definitions/capabilities/linear.capabilities.json +0 -378
  240. package/integrations/definitions/capabilities/netsuite.capabilities.json +0 -535
  241. package/integrations/definitions/capabilities/notion.capabilities.json +0 -208
  242. package/integrations/definitions/capabilities/phone.capabilities.json +0 -511
  243. package/integrations/definitions/capabilities/pipedrive.capabilities.json +0 -430
  244. package/integrations/definitions/capabilities/quickbooks.capabilities.json +0 -635
  245. package/integrations/definitions/capabilities/salesforce.capabilities.json +0 -568
  246. package/integrations/definitions/capabilities/shopify.capabilities.json +0 -1128
  247. package/integrations/definitions/capabilities/slack.capabilities.json +0 -373
  248. package/integrations/definitions/capabilities/stripe.capabilities.json +0 -630
  249. package/integrations/definitions/capabilities/webflow.capabilities.json +0 -196
  250. package/integrations/definitions/capabilities/zendesk.capabilities.json +0 -422
  251. package/integrations/definitions/compile.js +0 -80
  252. package/integrations/definitions/config-form.js +0 -137
  253. package/integrations/definitions/examples/acme-erp-quickbooks.definition.json +0 -467
  254. package/integrations/definitions/examples/asana-slack.definition.json +0 -219
  255. package/integrations/definitions/examples/calendly-salesforce.definition.json +0 -459
  256. package/integrations/definitions/examples/docusign-salesforce.definition.json +0 -305
  257. package/integrations/definitions/examples/fathom-hubspot-notion.definition.json +0 -575
  258. package/integrations/definitions/examples/github-linear.definition.json +0 -322
  259. package/integrations/definitions/examples/hubspot-slack.definition.json +0 -427
  260. package/integrations/definitions/examples/klaviyo-hubspot.definition.json +0 -336
  261. package/integrations/definitions/examples/phone-hubspot.definition.json +0 -633
  262. package/integrations/definitions/examples/pipedrive-hubspot.definition.json +0 -136
  263. package/integrations/definitions/examples/pipedrive-phone.definition.json +0 -340
  264. package/integrations/definitions/examples/records/phone-hubspot.definition-version.json +0 -652
  265. package/integrations/definitions/examples/records/phone-hubspot.integration.json +0 -86
  266. package/integrations/definitions/examples/records/phone-hubspot.mappings.json +0 -39
  267. package/integrations/definitions/examples/records/phone-hubspot.workflow-run.json +0 -62
  268. package/integrations/definitions/examples/shopify-klaviyo.definition.json +0 -488
  269. package/integrations/definitions/examples/shopify-netsuite.definition.json +0 -622
  270. package/integrations/definitions/examples/stripe-quickbooks.definition.json +0 -476
  271. package/integrations/definitions/examples/webflow-hubspot-slack.definition.json +0 -262
  272. package/integrations/definitions/examples/zendesk-jira.definition.json +0 -604
  273. package/integrations/definitions/expressions.js +0 -229
  274. package/integrations/definitions/index.js +0 -48
  275. package/integrations/definitions/integration-runtime.js +0 -779
  276. package/integrations/definitions/interpreter.js +0 -104
  277. package/integrations/definitions/manifests.js +0 -177
  278. package/integrations/definitions/renderers/common.js +0 -67
  279. package/integrations/definitions/renderers/html.js +0 -129
  280. package/integrations/definitions/renderers/hubspot.js +0 -59
  281. package/integrations/definitions/renderers/preview.js +0 -154
  282. package/integrations/definitions/renderers/slack.js +0 -146
  283. package/integrations/definitions/repositories/definition-version-repository.js +0 -67
  284. package/integrations/definitions/repositories/workflow-run-repository.js +0 -80
  285. package/integrations/definitions/router.js +0 -170
  286. package/integrations/definitions/runtime.js +0 -268
  287. package/integrations/definitions/schema/capability-manifest.v0.1.schema.json +0 -549
  288. package/integrations/definitions/schema/installation.v0.1.schema.json +0 -130
  289. package/integrations/definitions/schema/integration-definition.v0.1.schema.json +0 -909
  290. package/integrations/definitions/schema/records.md +0 -105
  291. package/integrations/definitions/schema-check.js +0 -58
  292. package/integrations/definitions/scripts/check-manifests.js +0 -220
  293. package/integrations/definitions/scripts/validate.js +0 -66
  294. package/integrations/definitions/steps.js +0 -123
  295. package/integrations/definitions/sync-engine.js +0 -297
  296. package/integrations/definitions/tests/helpers-fake-crm.js +0 -49
  297. package/integrations/definitions/transforms.js +0 -91
  298. package/integrations/definitions/use-cases/definition-use-cases.js +0 -245
  299. package/integrations/definitions/validate.js +0 -436
  300. package/integrations/definitions/versioning.js +0 -176
  301. package/integrations/definitions/webhooks.js +0 -119
  302. package/integrations/integration-types.js +0 -73
  303. package/integrations/triggers.js +0 -244
  304. package/integrations/use-cases/run-trigger-tick.js +0 -155
  305. package/integrations/use-cases/run-trigger.js +0 -77
  306. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +0 -66
  307. package/prisma-postgresql/migrations/20261019000000_workflow_run_waits/migration.sql +0 -7
  308. package/prisma-postgresql/migrations/20261019120000_definition_workspace/migration.sql +0 -5
@@ -1,154 +0,0 @@
1
- /**
2
- * Previews: what an integration will show inside the apps it connects, rendered from the current settings and sample
3
- * or real records. Same code path as production for everything that decides the content (queries, view resolution,
4
- * the interpreter and its steps); the only thing swapped is where a capability call goes.
5
- *
6
- * previewSurface a UI surface (a HubSpot card, a Slack message menu...) -> view + html (+ slack blocks / hubspot tree)
7
- * previewWorkflow dry-run a workflow: every outbound call is RECORDED with the args it would send, none is made
8
- */
9
- const { makePorts, renderSurface, runWorkflow, recordOf } = require('../runtime');
10
- const { isBuiltin } = require('../steps');
11
- const { viewToSlack } = require('./slack');
12
- const { viewToHubSpot } = require('./hubspot');
13
- const { viewToHtml, slackMessageToHtml, callToHtml, slackBlocksToHtml, esc, HOSTS } = require('./html');
14
-
15
- class DryRunError extends Error { constructor(what) { super(`dry run: ${what} was not performed`); this.name = 'DryRunError'; } }
16
-
17
- const clone = (v) => (v === undefined ? v : JSON.parse(JSON.stringify(v)));
18
-
19
- /** A plausible empty record for a manifest object: ids are filled (a step that checks `$.steps.x.id exists` proceeds), the rest is empty of its type. */
20
- function sampleObject(manifest, name) {
21
- const o = manifest?.objects?.[name];
22
- const out = {};
23
- for (const f of o?.fields || []) {
24
- const keys = String(f.name).split('.');
25
- let cur = out;
26
- keys.slice(0, -1).forEach((k) => { cur = (cur[k] = cur[k] && typeof cur[k] === 'object' ? cur[k] : {}); });
27
- const empty = f.type === 'id' ? `preview-${name}-${f.name}` : f.type === 'number' ? 0 : f.type === 'boolean' ? false : f.type === 'json' ? {} : f.type === 'array' ? [] : '';
28
- cur[keys[keys.length - 1]] = empty;
29
- }
30
- return out;
31
- }
32
- function sampleOutput(manifest, cap) {
33
- const o = String(cap?.output || '');
34
- if (o.endsWith('[]')) return [];
35
- if (/^page</.test(o)) return { results: [] };
36
- return o && manifest?.objects?.[o] ? sampleObject(manifest, o) : {};
37
- }
38
-
39
- /**
40
- * Ports for previews. `ports.call` answers from `fixtures` (key "<module key>.<capability>", a value or a function of
41
- * the args), else from the manifest's own output shape, and records the call. The real `callApi` the base ports would
42
- * use throws, so a call that somehow bypassed the recorder fails loudly instead of reaching an API.
43
- *
44
- * @param {{ def, manifests, fixtures?: object, store?: Map }} p fixtures.store pre-seeds the link store (keys as the real store's)
45
- * @returns {{ ports, calls: object[], refusing: object }} `refusing`: the unwrapped ports, whose call() always throws (kept so a test can prove it)
46
- */
47
- function recordingPorts({ def, manifests, fixtures = {}, store }) {
48
- const calls = [];
49
- const rows = store || new Map(Object.entries(clone(fixtures.store) || {}));
50
- const mem = {
51
- get: async (k) => rows.get(k) ?? null,
52
- put: async (k, v) => { rows.set(k, v); },
53
- claim: async (k, v) => { if (rows.has(k)) return false; rows.set(k, v); return true; },
54
- release: async (k) => { rows.delete(k); },
55
- };
56
- const base = makePorts({
57
- def, manifests, store: mem,
58
- callApi: async (moduleKey, method) => { throw new DryRunError(`${moduleKey}.${method}`); },
59
- runCode: async (ref, args) => answer('code', ref, 'code', args, fixtures[`code:${ref}`], {}),
60
- });
61
- const ports = { ...base }; // the seam below replaces call() on this copy; `base` keeps the one that reaches callApi
62
- const manifestFor = (key) => manifests[def.modules[key]?.module];
63
- async function answer(module, capability, kind, args, fixture, fallback) {
64
- const call = { module, capability, kind, args: clone(args) };
65
- calls.push(call);
66
- try {
67
- call.output = typeof fixture === 'function' ? clone(await fixture(clone(args), { module, capability })) : fixture !== undefined ? clone(fixture) : fallback;
68
- return clone(call.output);
69
- } catch (e) { call.error = String(e.message || e); throw e; }
70
- }
71
- // `refusing` is the base ports as they were before the seam below: their call() reaches callApi, which throws.
72
- // The seam: the interpreter only ever reaches a module through ports.call.
73
- ports.call = async (moduleKey, capability, args) => {
74
- const m = manifestFor(moduleKey);
75
- const c = m?.capabilities?.[capability];
76
- if (!c) {
77
- calls.push({ module: moduleKey, capability, kind: 'unknown', args: clone(args), error: 'not declared by the module manifest' });
78
- throw new Error(`${moduleKey}.${capability} is not declared by the module manifest`);
79
- }
80
- return answer(moduleKey, capability, c.kind, args, fixtures[`${moduleKey}.${capability}`], sampleOutput(m, c));
81
- };
82
- return { ports, calls, refusing: base };
83
- }
84
-
85
- /** read/search/subscribe calls fetch; the rest (and code) is what leaves the integration. */
86
- const isSend = (call) => call.kind === 'write' || call.kind === 'delete';
87
-
88
- /** How one outbound call would look where it lands. Slack messages get the real Block Kit/mrkdwn rendering. */
89
- function renderCall(call, manifest, appName) {
90
- if (manifest?.module === 'slack' && /^message\./.test(call.capability)) {
91
- const a = call.args || {};
92
- const slack = { channel: a.channel, text: a.text, ...(Array.isArray(a.blocks) ? { blocks: a.blocks } : {}) };
93
- return { html: slackMessageToHtml({ ...slack, appName }), slack };
94
- }
95
- return { html: callToHtml(call) };
96
- }
97
-
98
- /**
99
- * Dry-run one workflow. Nothing is performed: every capability call is recorded with the args it would carry and
100
- * answered from `fixtures` (or an empty record shaped by the manifest). A webhook workflow with no `event` runs on
101
- * the manifest's own sample payload for its trigger.
102
- *
103
- * @param {{ def, installation?, workflowId: string, event?, record?, manifests, fixtures?: object, now?: Function }} p
104
- * @returns {Promise<{ workflow: string, status: string, reason?: string, error?: string, waiting?: object, sends: object[], calls: object[], trace: object[] }>}
105
- * `calls`: every call in order, `{ stepId, at, module, capability, kind, args, output, rendered }`; `sends`: the writes
106
- * among them (`rendered` is `{ html, slack? }` for those, null for reads)
107
- */
108
- async function previewWorkflow({ def, installation = {}, workflowId, event, record, manifests, fixtures = {}, now }) {
109
- const w = (def.workflows || []).find((x) => x.id === workflowId);
110
- if (!w) throw new Error(`Unknown workflow "${workflowId}"`);
111
- if (event === undefined) {
112
- const t = w.trigger;
113
- event = (t.type === 'webhook' && clone(manifests[def.modules[t.module]?.module]?.events?.[t.topic]?.payload)) || {};
114
- }
115
- const { ports, calls } = recordingPorts({ def, manifests, fixtures });
116
- const run = await runWorkflow({ def, installation, workflowId, event, record, ports, manifests, ...(now ? { now } : {}) });
117
-
118
- // The trace has one entry per step that ran; the calls, in the same order, belong to the entries that are capability steps (or code).
119
- const callSteps = (run.trace || []).filter((t) => (!isBuiltin(t.use) || t.use === 'code') && (t.status === 'ok' || t.status === 'error'));
120
- const aligned = callSteps.length === calls.length;
121
- const out = calls.map((c, i) => {
122
- const manifest = manifests[def.modules[c.module]?.module];
123
- return { stepId: aligned ? callSteps[i].id : null, at: aligned ? callSteps[i].at : null, ...c, rendered: isSend(c) && manifest ? renderCall(c, manifest, def.label || def.id) : null };
124
- });
125
- const waiting = run.status === 'waiting' ? run.waitingFor : undefined;
126
- return {
127
- workflow: workflowId, status: run.status, ...(run.reason ? { reason: run.reason } : {}), ...(run.error ? { error: run.error } : {}),
128
- ...(waiting ? { waiting: { ...waiting, ...(waiting.kind === 'approval' ? { html: `<div style="${HOSTS.generic.frame}"><div style="${HOSTS.generic.head}">Waits for approval</div><div style="${HOSTS.generic.body}">${esc(waiting.prompt)}</div></div>` } : {}) } } : {}),
129
- sends: out.filter(isSend), calls: out, trace: run.trace || [],
130
- };
131
- }
132
-
133
- /**
134
- * Render one UI surface. `ports` answer the surface's query (real ports for real records; `recordingPorts(...).ports`
135
- * with fixtures for samples). `host` defaults to the surface's own.
136
- *
137
- * @returns {Promise<{ surface, view, html: string, slack?: { blocks, text, truncated }, hubspot?: object }>}
138
- */
139
- async function previewSurface({ def, installation = {}, surfaceId, record, ports, host }) {
140
- const { surface, view } = await renderSurface({ def, installation, surfaceId, record, ports });
141
- const h = host || surface.host;
142
- const opts = { integrationId: installation.integrationId, surface: surface.id };
143
- const out = { surface, view };
144
- if (h === 'slack') {
145
- out.slack = viewToSlack(view, opts);
146
- out.html = `<div data-preview-host="slack" style="${HOSTS.slack.frame}"><div style="${HOSTS.slack.head}">${esc(surface.label || def.label || def.id)} · ${esc(surface.placement)}</div><div style="${HOSTS.slack.body}">${view ? slackBlocksToHtml(out.slack.blocks) : '<em>Nothing to show for this record.</em>'}</div></div>`;
147
- } else {
148
- if (h === 'hubspot') out.hubspot = viewToHubSpot(view, opts);
149
- out.html = viewToHtml(view, { host: h, title: `${def.ui.find((u) => u.id === surface.id)?.label || surface.id} · ${surface.placement}` });
150
- }
151
- return out;
152
- }
153
-
154
- module.exports = { previewSurface, previewWorkflow, recordingPorts, DryRunError, recordOf };
@@ -1,146 +0,0 @@
1
- /**
2
- * Resolved view tree -> Slack Block Kit. Data comes from vendor records, so everything that is data is escaped for
3
- * mrkdwn, and everything is cut to Slack's limits with a visible marker instead of letting chat.postMessage reject the
4
- * whole message (a long deal name must not cost the approval request).
5
- */
6
- const { str, safeUrl, truncate, toneOf, encodeAction } = require('./common');
7
-
8
- const LIMITS = { blocks: 50, header: 150, sectionText: 3000, fieldText: 2000, fields: 10, contextText: 3000, contextElements: 10, button: 75, buttonUrl: 3000, actionId: 255, actions: 25, listItems: 25, itemText: 300 };
9
- const MARK = '…';
10
- const ZWSP = '\u200b';
11
-
12
- /**
13
- * Text that is DATA, for a mrkdwn field. `&`, `<` and `>` are the three characters Slack requires escaping (this also
14
- * defuses `<!channel>` and `<https://evil|click>` in a vendor field). Slack has no backslash escape for `* _ ~ \``, so
15
- * `* _ ~` are fenced with a zero-width space, which stops them pairing into bold/italic/strike without changing how they
16
- * look. A fenced backtick would still pair into a code span, so it becomes the look-alike U+02CB.
17
- */
18
- function escapeMrkdwn(v) {
19
- return str(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/[*_~]/g, `${ZWSP}$&${ZWSP}`).replace(/`/g, '\u02cb');
20
- }
21
- const mrkdwn = (t) => ({ type: 'mrkdwn', text: t });
22
- const plain = (t) => ({ type: 'plain_text', text: t, emoji: false });
23
- const clip = (s, max) => truncate(s, max, MARK);
24
-
25
- /** Lines packed into as few sections as fit 3000 characters; one line is never split. */
26
- function linesToSections(lines) {
27
- const out = [];
28
- let cur = '';
29
- for (const l of lines) {
30
- const line = clip(l, LIMITS.itemText);
31
- if (cur && cur.length + 1 + line.length > LIMITS.sectionText) { out.push(cur); cur = ''; }
32
- cur = cur ? `${cur}\n${line}` : line;
33
- }
34
- if (cur) out.push(cur);
35
- return out.map((text) => ({ type: 'section', text: mrkdwn(clip(text, LIMITS.sectionText)) }));
36
- }
37
-
38
- const TONE_EMOJI = { positive: ':large_green_circle:', warning: ':large_yellow_circle:', critical: ':red_circle:', neutral: ':white_circle:' };
39
-
40
- /**
41
- * @param {object|null} view resolveView output ({ component, props, children })
42
- * @param {{ integrationId?: string, surface?: string }} [opts] what a button's click carries back
43
- * @returns {{ blocks: object[], text: string, truncated: boolean }} `text` is the notification/fallback text
44
- */
45
- function viewToSlack(view, { integrationId, surface } = {}) {
46
- const blocks = [];
47
- let text = '';
48
- let truncated = false;
49
- let pending = []; // consecutive buttons share one actions block
50
- let n = 0;
51
-
52
- const flush = () => {
53
- if (!pending.length) return;
54
- const els = pending.slice(0, LIMITS.actions);
55
- if (pending.length > LIMITS.actions) truncated = true;
56
- blocks.push({ type: 'actions', elements: els });
57
- pending = [];
58
- };
59
- const push = (b) => { flush(); blocks.push(b); };
60
-
61
- const walk = (node) => {
62
- if (!node || typeof node !== 'object') return;
63
- const p = node.props || {};
64
- switch (node.component) {
65
- case 'Heading': {
66
- flush();
67
- const t = clip(str(p.text), LIMITS.header);
68
- if (!text) text = t;
69
- if (t) blocks.push({ type: 'header', text: plain(t) });
70
- break;
71
- }
72
- case 'Text': {
73
- const t = str(p.text);
74
- if (!text) text = t;
75
- if (t) push({ type: 'section', text: mrkdwn(clip(escapeMrkdwn(t), LIMITS.sectionText)) });
76
- break;
77
- }
78
- case 'KeyValue': {
79
- const items = Array.isArray(p.items) ? p.items : [];
80
- for (let i = 0; i < items.length; i += LIMITS.fields) {
81
- const fields = items.slice(i, i + LIMITS.fields).map((it) => mrkdwn(clip(`*${escapeMrkdwn(it?.label)}*\n${escapeMrkdwn(it?.value) || '—'}`, LIMITS.fieldText)));
82
- if (fields.length) push({ type: 'section', fields });
83
- }
84
- break;
85
- }
86
- case 'List': {
87
- const items = Array.isArray(p.items) ? p.items : [];
88
- const lines = items.slice(0, LIMITS.listItems).map((it) => `• ${escapeMrkdwn(it?.title)}${it?.meta ? ` — _${escapeMrkdwn(it.meta)}_` : ''}`);
89
- if (items.length > LIMITS.listItems) { lines.push(`_${MARK} and ${items.length - LIMITS.listItems} more_`); truncated = true; }
90
- flush();
91
- blocks.push(...linesToSections(lines));
92
- break;
93
- }
94
- case 'Table': {
95
- const cols = Array.isArray(p.columns) ? p.columns : [];
96
- const rows = Array.isArray(p.rows) ? p.rows : [];
97
- const lines = [];
98
- if (cols.length) lines.push(cols.map((c) => `*${escapeMrkdwn(c)}*`).join(' | '));
99
- for (const r of rows.slice(0, LIMITS.listItems)) lines.push((Array.isArray(r) ? r : []).map((c) => escapeMrkdwn(c) || '—').join(' | '));
100
- if (rows.length > LIMITS.listItems) { lines.push(`_${MARK} and ${rows.length - LIMITS.listItems} more_`); truncated = true; }
101
- flush();
102
- blocks.push(...linesToSections(lines));
103
- break;
104
- }
105
- case 'Badge': {
106
- const t = str(p.text);
107
- if (t) push({ type: 'context', elements: [mrkdwn(clip(`${TONE_EMOJI[toneOf(p.tone)]} *${escapeMrkdwn(t)}*`, LIMITS.contextText))] });
108
- break;
109
- }
110
- case 'Empty': {
111
- const t = str(p.text);
112
- if (t) push({ type: 'context', elements: [mrkdwn(clip(`_${escapeMrkdwn(t)}_`, LIMITS.contextText))] });
113
- break;
114
- }
115
- case 'Divider': push({ type: 'divider' }); break;
116
- case 'Button': {
117
- const label = clip(str(p.label) || 'Open', LIMITS.button);
118
- const style = p.style === 'primary' ? { style: 'primary' } : p.style === 'danger' ? { style: 'danger' } : {};
119
- const a = p.action;
120
- if (a?.type === 'userAction' && typeof a.workflow === 'string') {
121
- const value = encodeAction({ integrationId, workflow: a.workflow, surface, needsInput: !!a.input });
122
- if (value) pending.push({ type: 'button', text: plain(label), action_id: clip(`frigg:${a.workflow}`, LIMITS.actionId - 8) + `:${n++}`, value, ...style });
123
- } else if (a?.type === 'open_url') {
124
- const url = safeUrl(a.url);
125
- if (url && url.length <= LIMITS.buttonUrl) pending.push({ type: 'button', text: plain(label), action_id: `frigg:open:${n++}`, url });
126
- }
127
- break;
128
- }
129
- default: break; // Stack, and anything a newer definition adds: just its children
130
- }
131
- for (const c of node.children || []) walk(c);
132
- };
133
- walk(view);
134
- flush();
135
-
136
- // 50 blocks is Slack's whole-message cap. Keep 49 and say so, rather than lose the message.
137
- let out = blocks;
138
- if (blocks.length > LIMITS.blocks) {
139
- out = [...blocks.slice(0, LIMITS.blocks - 1), { type: 'context', elements: [mrkdwn(`_${MARK} ${blocks.length - (LIMITS.blocks - 1)} more blocks not shown_`)] }];
140
- truncated = true;
141
- }
142
- // `text` is the notification fallback and Slack parses it as mrkdwn too, so a record's `<!channel>` must not ping from there either.
143
- return { blocks: out, text: clip(escapeMrkdwn(text || 'Update'), LIMITS.sectionText), truncated };
144
- }
145
-
146
- module.exports = { viewToSlack, escapeMrkdwn, LIMITS };
@@ -1,67 +0,0 @@
1
- /**
2
- * IntegrationDefinitionVersion persistence (ADR-063). Pure data access: one Prisma call per method, no business rules.
3
- * Which version is "latest", whether a publish is allowed and what a status change means are decided by the use cases
4
- * (../use-cases/definition-use-cases.js); this layer only stores and reads rows.
5
- *
6
- * Rows come back as plain objects with string ids on every database. `document` is the definition verbatim.
7
- */
8
- const { prisma } = require('../../../database/prisma');
9
-
10
- const FIELDS = ['definitionId', 'version', 'schemaVersion', 'status', 'checksum', 'document', 'changeLevel', 'previousVersion', 'notes', 'createdBy', 'workspaceId', 'publishedAt'];
11
- const pick = (o) => Object.fromEntries(FIELDS.filter((k) => o[k] !== undefined).map((k) => [k, o[k]]));
12
-
13
- class DefinitionVersionRepositoryMongo {
14
- constructor({ client } = {}) { this.prisma = client || prisma; }
15
- toRow(r) { return r ? { ...r, id: String(r.id) } : null; }
16
-
17
- async findVersion(definitionId, version) {
18
- return this.toRow(await this.prisma.integrationDefinitionVersion.findUnique({ where: { definitionId_version: { definitionId, version } } }));
19
- }
20
-
21
- /** Every version of one definition, any status, unordered (semver order is the caller's business). */
22
- async listVersions(definitionId) {
23
- return (await this.prisma.integrationDefinitionVersion.findMany({ where: { definitionId } })).map((r) => this.toRow(r));
24
- }
25
-
26
- /** Rows in the given statuses across all definitions, without documents (a catalogue listing). */
27
- async listByStatus(statuses) {
28
- const rows = await this.prisma.integrationDefinitionVersion.findMany({
29
- where: { status: { in: statuses } },
30
- select: { id: true, definitionId: true, version: true, schemaVersion: true, status: true, checksum: true, changeLevel: true, workspaceId: true, publishedAt: true, createdAt: true },
31
- });
32
- return rows.map((r) => this.toRow(r));
33
- }
34
-
35
- async createVersion(data) {
36
- return this.toRow(await this.prisma.integrationDefinitionVersion.create({ data: pick(data) }));
37
- }
38
-
39
- /**
40
- * Change a row only if it is still in `fromStatus` (the use case's precondition, checked by the database, so two
41
- * concurrent publishes cannot both win). Returns the updated row, or null if it was not in that status.
42
- */
43
- async updateIfStatus(definitionId, version, fromStatus, data) {
44
- const { count } = await this.prisma.integrationDefinitionVersion.updateMany({ where: { definitionId, version, status: fromStatus }, data: pick(data) });
45
- return count ? this.findVersion(definitionId, version) : null;
46
- }
47
-
48
- async deleteIfStatus(definitionId, version, status) {
49
- const { count } = await this.prisma.integrationDefinitionVersion.deleteMany({ where: { definitionId, version, status } });
50
- return count > 0;
51
- }
52
- }
53
-
54
- class DefinitionVersionRepositoryPostgres extends DefinitionVersionRepositoryMongo {}
55
- class DefinitionVersionRepositoryDocumentDB extends DefinitionVersionRepositoryMongo {}
56
-
57
- function createDefinitionVersionRepository() {
58
- const { DB_TYPE } = require('../../../database/config');
59
- switch (DB_TYPE) {
60
- case 'mongodb': return new DefinitionVersionRepositoryMongo();
61
- case 'postgresql': return new DefinitionVersionRepositoryPostgres();
62
- case 'documentdb': return new DefinitionVersionRepositoryDocumentDB();
63
- default: throw new Error(`Unsupported database type: ${DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`);
64
- }
65
- }
66
-
67
- module.exports = { createDefinitionVersionRepository, DefinitionVersionRepositoryMongo, DefinitionVersionRepositoryPostgres, DefinitionVersionRepositoryDocumentDB };
@@ -1,80 +0,0 @@
1
- /**
2
- * WorkflowRun persistence (ADR-063). Pure data access. A run row never holds request or response bodies: `trace` is
3
- * step ids, statuses, timings and error messages (see runtime.js).
4
- */
5
- const { prisma } = require('../../../database/prisma');
6
-
7
- class WorkflowRunRepositoryMongo {
8
- constructor({ client } = {}) { this.prisma = client || prisma; }
9
- toId(id) { return String(id); }
10
- toRowId(id) { return String(id); }
11
- toRow(r) { return r ? { ...r, id: String(r.id), integrationId: String(r.integrationId) } : null; }
12
-
13
- async createRun(run) {
14
- return this.toRow(await this.prisma.workflowRun.create({
15
- data: {
16
- integrationId: this.toId(run.integrationId), definitionId: run.definitionId, definitionVersion: run.definitionVersion,
17
- workflowId: run.workflowId, triggerType: run.triggerType, eventId: run.eventId ?? null, status: run.status,
18
- reason: run.reason ?? null, error: run.error ?? null, retryable: Boolean(run.retryable), trace: run.trace || [],
19
- runKey: run.runKey ?? null, waitingFor: run.waitingFor ?? null, resumeAt: run.resumeAt ? new Date(run.resumeAt) : null,
20
- startedAt: new Date(run.startedAt), endedAt: run.endedAt ? new Date(run.endedAt) : null,
21
- },
22
- }));
23
- }
24
-
25
- /** One run of an installation, by the runtime's run id. */
26
- async findRun(integrationId, runKey) {
27
- return this.toRow(await this.prisma.workflowRun.findFirst({ where: { integrationId: this.toId(integrationId), runKey: String(runKey) }, orderBy: { startedAt: 'desc' } }));
28
- }
29
-
30
- /** Update a run by its row id (a paused run finishing, or pausing again). Only the given fields change. */
31
- async updateRun(id, data) {
32
- const fields = {};
33
- for (const k of ['status', 'reason', 'error', 'retryable', 'trace', 'waitingFor']) if (k in data) fields[k] = data[k] ?? (k === 'trace' ? [] : null);
34
- if ('resumeAt' in data) fields.resumeAt = data.resumeAt ? new Date(data.resumeAt) : null;
35
- if ('endedAt' in data) fields.endedAt = data.endedAt ? new Date(data.endedAt) : null;
36
- return this.toRow(await this.prisma.workflowRun.update({ where: { id: this.toRowId(id) }, data: fields }));
37
- }
38
-
39
- /** Newest first. `before` pages by startedAt. */
40
- async listRuns(integrationId, { workflowId, status, before, limit = 50 } = {}) {
41
- const rows = await this.prisma.workflowRun.findMany({
42
- where: { integrationId: this.toId(integrationId), ...(workflowId ? { workflowId } : {}), ...(status ? { status } : {}), ...(before ? { startedAt: { lt: new Date(before) } } : {}) },
43
- orderBy: { startedAt: 'desc' },
44
- take: Math.min(Math.max(1, limit), 500),
45
- });
46
- return rows.map((r) => this.toRow(r));
47
- }
48
-
49
- /** Delete runs that started before `olderThan`, optionally only in some statuses. Returns the count. */
50
- async deleteRunsBefore(olderThan, { statuses } = {}) {
51
- const { count } = await this.prisma.workflowRun.deleteMany({ where: { startedAt: { lt: new Date(olderThan) }, ...(statuses ? { status: { in: statuses } } : {}) } });
52
- return count;
53
- }
54
- }
55
-
56
- class WorkflowRunRepositoryPostgres extends WorkflowRunRepositoryMongo {
57
- toId(id) {
58
- const n = Number.parseInt(id, 10);
59
- if (Number.isNaN(n)) throw new Error(`Invalid integration id: ${id}`);
60
- return n;
61
- }
62
- toRowId(id) {
63
- const n = Number.parseInt(id, 10);
64
- if (Number.isNaN(n)) throw new Error(`Invalid run id: ${id}`);
65
- return n;
66
- }
67
- }
68
- class WorkflowRunRepositoryDocumentDB extends WorkflowRunRepositoryMongo {}
69
-
70
- function createWorkflowRunRepository() {
71
- const { DB_TYPE } = require('../../../database/config');
72
- switch (DB_TYPE) {
73
- case 'mongodb': return new WorkflowRunRepositoryMongo();
74
- case 'postgresql': return new WorkflowRunRepositoryPostgres();
75
- case 'documentdb': return new WorkflowRunRepositoryDocumentDB();
76
- default: throw new Error(`Unsupported database type: ${DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`);
77
- }
78
- }
79
-
80
- module.exports = { createWorkflowRunRepository, WorkflowRunRepositoryMongo, WorkflowRunRepositoryPostgres, WorkflowRunRepositoryDocumentDB };
@@ -1,170 +0,0 @@
1
- /**
2
- * HTTP routes for integration versions, run history, sync progress and approvals, mounted by createIntegrationRouter
3
- * when the app registers the integration runtime (a class with `IntegrationRuntime`). They answer for EVERY integration,
4
- * however it was made: an integration with no run history, syncs or other versions gets an empty answer, not a 404.
5
- * Thin adapters: authenticate, call a use case, map the result to a status.
6
- *
7
- * Signed-in users (Frigg's usual authentication: bearer token, x-frigg headers or adopter JWT):
8
- * GET /api/integration-definitions what can be installed: latest published version of each
9
- * (GET /api/integrations lists the same types as options)
10
- * GET /api/integrations/:integrationId/upgrade?to= the upgrade plan for an installation (changes nothing)
11
- * POST /api/integrations/:integrationId/upgrade { to? } apply it (refused while anything is needed)
12
- * GET /api/integrations/:integrationId/runs workflow run history, newest first (?workflowId&status&before&limit)
13
- * GET /api/integrations/:integrationId/syncs each sync's backfill progress and poll marks
14
- * POST /api/integrations/:integrationId/runs/:runId/decision { decision: approve|reject, note? } on a paused approval
15
- *
16
- * A workspace's own integrations (signed-in user; the workspace is the user's Frigg id):
17
- * POST /api/integration-definitions { document, notes?, publish? = true } publish (or draft) a version
18
- * GET /api/integration-definitions/:id its versions and the latest document, for the editor
19
- *
20
- * App admins (x-frigg-admin-api-key, the validateAdminApiKey middleware that /admin/scripts and /admin/db-migrate use):
21
- * POST /admin/integrations/versions { document, notes?, publish? = true, strict? } validate + publish
22
- * GET /admin/integrations/:id/versions every version of one integration
23
- * POST /admin/integrations/:id/versions/:version/deprecate
24
- *
25
- * Installing needs no new route: POST /api/integrations with config { type: '<integration id>' } pins the latest
26
- * published version, exactly as a code integration is installed by its name.
27
- */
28
- const catchAsyncError = require('express-async-handler');
29
- const { validateAdminApiKey } = require('../../handlers/middleware/admin-auth');
30
- const { DefinitionError, PublishDefinitionVersion, DeprecateDefinitionVersion, GetDefinitionHistory, GetIntegrationForDecision, GetOwnDefinition } = require('./use-cases/definition-use-cases');
31
-
32
- /** An integration with one version (code that is not published as versions): nothing to move to. */
33
- const noUpgrade = (integration) => ({
34
- allowed: false, applied: false, needs: [], actions: [],
35
- from: integration.constructor?.Definition?.version ?? null, to: integration.constructor?.Definition?.version ?? null,
36
- reason: 'This integration has no other version to move to',
37
- });
38
-
39
- /**
40
- * Every route this router registers, in one place: method, path and who may call it. The registrations below take their
41
- * paths from here, so this list cannot drift from what is served.
42
- *
43
- * Admin routes sit in core's existing /admin/* namespace (beside /admin/scripts and /admin/db-migrate, same admin-auth
44
- * middleware). The user routes are on their pre-/api/v2 paths and move onto the /api/v2 route registry via PR #671; having
45
- * them listed as data is what makes that move mechanical.
46
- * auth: 'user' = a signed-in user (the workspace is the user's Frigg id), 'admin' = x-frigg-admin-api-key.
47
- */
48
- const DEFINITION_ROUTES = Object.freeze({
49
- catalogue: { method: 'GET', path: '/api/integration-definitions', auth: 'user' },
50
- upgradePlan: { method: 'GET', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
51
- upgradeApply: { method: 'POST', path: '/api/integrations/:integrationId/upgrade', auth: 'user' },
52
- runs: { method: 'GET', path: '/api/integrations/:integrationId/runs', auth: 'user' },
53
- decide: { method: 'POST', path: '/api/integrations/:integrationId/runs/:runId/decision', auth: 'user' },
54
- syncs: { method: 'GET', path: '/api/integrations/:integrationId/syncs', auth: 'user' },
55
- ownPublish: { method: 'POST', path: '/api/integration-definitions', auth: 'user' },
56
- ownRead: { method: 'GET', path: '/api/integration-definitions/:definitionId', auth: 'user' },
57
- adminPublish: { method: 'POST', path: '/admin/integrations/versions', auth: 'admin' },
58
- adminVersions: { method: 'GET', path: '/admin/integrations/:definitionId/versions', auth: 'admin' },
59
- adminDeprecate: { method: 'POST', path: '/admin/integrations/:definitionId/versions/:version/deprecate', auth: 'admin' },
60
- });
61
- const path = (key) => DEFINITION_ROUTES[key].path;
62
-
63
- function sendError(res, e) {
64
- if (e instanceof DefinitionError || e?.name === 'DefinitionError') return res.status(e.status || 400).json({ error: e.message, code: e.code, details: e.details });
65
- throw e;
66
- }
67
- const guard = (fn) => catchAsyncError(async (req, res) => { try { await fn(req, res); } catch (e) { sendError(res, e); } });
68
-
69
- /**
70
- * @param {import('express').Router} router
71
- * @param {{ execute(req): Promise<{ getId(): string }> }} authenticateUser
72
- * @param {{ getIntegrationInstance, integrationClasses: Function[] }} deps
73
- */
74
- function setIntegrationRuntimeRoutes(router, authenticateUser, { getIntegrationInstance, integrationClasses }) {
75
- const classes = integrationClasses.filter((c) => c.IntegrationRuntime);
76
- if (!classes.length) return false;
77
- const catalogClass = classes.find((c) => c.IntegrationRuntime.shared) || classes[0];
78
- const { definitions, manifests } = catalogClass.IntegrationRuntime;
79
-
80
- const installation = async (req) => {
81
- const user = await authenticateUser.execute(req);
82
- return getIntegrationInstance.execute(req.params.integrationId, user.getId());
83
- };
84
- const runtimeOf = (integration) => integration?.constructor?.IntegrationRuntime || null;
85
-
86
- router.route(path('catalogue')).get(guard(async (req, res) => {
87
- const user = await authenticateUser.execute(req);
88
- const lists = await Promise.all(classes.map((c) => (c.IntegrationRuntime.definitions.list ? c.IntegrationRuntime.definitions.list({ workspaceId: String(user.getId()) }) : [])));
89
- const seen = new Set();
90
- res.json({ definitions: lists.flat().filter((d) => (seen.has(d.id) ? false : seen.add(d.id))) });
91
- }));
92
-
93
- router.route(path('upgradePlan'))
94
- .get(guard(async (req, res) => {
95
- const integration = await installation(req);
96
- if (typeof integration.upgradeTo !== 'function') return res.json(noUpgrade(integration));
97
- res.json(await integration.upgradeTo(req.query.to || undefined, { apply: false }));
98
- }))
99
- .post(guard(async (req, res) => {
100
- const integration = await installation(req);
101
- if (typeof integration.upgradeTo !== 'function') return res.status(409).json(noUpgrade(integration));
102
- const result = await integration.upgradeTo(req.body?.to || undefined, { apply: true });
103
- res.status(result.applied ? 200 : 409).json(result);
104
- }));
105
-
106
- router.route(path('runs')).get(guard(async (req, res) => {
107
- const integration = await installation(req);
108
- const store = runtimeOf(integration)?.runs() || catalogClass.IntegrationRuntime.runs();
109
- if (!store) return res.json({ runs: [] });
110
- const { workflowId, status, before, limit } = req.query;
111
- res.json({ runs: await store.listRuns(integration.id, { workflowId, status, before, limit: limit ? Number(limit) : undefined }) });
112
- }));
113
-
114
- // Approvers need not own the installation: the run names who may decide it (an approval step's approvers).
115
- // Anyone else learns nothing, not even that the installation exists.
116
- router.route(path('decide')).post(guard(async (req, res) => {
117
- const user = await authenticateUser.execute(req);
118
- const { integration, isOwner } = await new GetIntegrationForDecision({ getIntegrationInstance }).execute(req.params.integrationId, user.getId());
119
- if (typeof integration?.decide !== 'function') throw new DefinitionError(`Run ${req.params.runId} is not waiting`, { status: 404, code: 'NOT_WAITING' });
120
- const out = await integration.decide(req.params.runId, { decision: req.body?.decision, note: req.body?.note, userId: user.getId(), isOwner });
121
- res.status(202).json(out);
122
- }));
123
-
124
- router.route(path('syncs')).get(guard(async (req, res) => {
125
- const integration = await installation(req);
126
- res.json({ syncs: typeof integration.syncStatus === 'function' ? await integration.syncStatus() : [] });
127
- }));
128
-
129
- // A workspace's own integrations: publish a version, read it back for the editor. The
130
- // integration then appears to that workspace like any other; no one else can see, read or install it.
131
- const registeredNames = new Set(integrationClasses.map((c) => c.Definition?.name).filter(Boolean));
132
- router.route(path('ownPublish')).post(guard(async (req, res) => {
133
- const user = await authenticateUser.execute(req);
134
- const { document, notes, publish } = req.body || {};
135
- const out = await new PublishDefinitionVersion({ repository: repository(), manifests, isReserved: (id) => registeredNames.has(id) })
136
- .execute({ document, notes, publish: publish !== false, workspaceId: String(user.getId()), createdBy: String(user.getId()) });
137
- const { document: _omit, ...version } = out.version;
138
- res.status(out.created ? 201 : 200).json({ ...out, version });
139
- }));
140
- router.route(path('ownRead')).get(guard(async (req, res) => {
141
- const user = await authenticateUser.execute(req);
142
- const out = await new GetOwnDefinition({ repository: repository() }).execute({ definitionId: req.params.definitionId, workspaceId: String(user.getId()) });
143
- res.json(out);
144
- }));
145
-
146
- const admin = (path) => router.route(path).all(validateAdminApiKey);
147
- const repository = () => {
148
- const repo = definitions.repository;
149
- if (!repo) throw new DefinitionError('This app runs bundled definitions; there is no store to publish to', { status: 501, code: 'NO_STORE' });
150
- return repo;
151
- };
152
-
153
- admin(path('adminPublish')).post(guard(async (req, res) => {
154
- const { document, notes, publish, strict, createdBy } = req.body || {};
155
- const out = await new PublishDefinitionVersion({ repository: repository(), manifests, isReserved: (id) => registeredNames.has(id) }).execute({ document, notes, createdBy, publish: publish !== false, strict: Boolean(strict) });
156
- const { document: _omit, ...version } = out.version;
157
- res.status(out.created ? 201 : 200).json({ ...out, version });
158
- }));
159
- admin(path('adminVersions')).get(guard(async (req, res) => {
160
- res.json({ versions: await new GetDefinitionHistory({ repository: repository() }).execute({ definitionId: req.params.definitionId }) });
161
- }));
162
- admin(path('adminDeprecate')).post(guard(async (req, res) => {
163
- const row = await new DeprecateDefinitionVersion({ repository: repository() }).execute(req.params);
164
- const { document: _omit, ...version } = row;
165
- res.json({ version });
166
- }));
167
- return true;
168
- }
169
-
170
- module.exports = { setIntegrationRuntimeRoutes, DEFINITION_ROUTES };