@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.693.d8859c2.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 (261) hide show
  1. package/CLAUDE.md +0 -28
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +10 -20
  4. package/application/commands/scheduler-commands.js +7 -11
  5. package/application/commands/user-commands.js +5 -17
  6. package/application/index.js +2 -6
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/repositories/credential-repository-documentdb.js +0 -34
  10. package/credential/repositories/credential-repository-interface.js +0 -15
  11. package/credential/repositories/credential-repository-mongo.js +0 -23
  12. package/credential/repositories/credential-repository-postgres.js +0 -28
  13. package/database/config.js +24 -48
  14. package/database/documentdb-utils.js +0 -2
  15. package/database/encryption/encryption-schema-registry.js +12 -93
  16. package/database/prisma.js +2 -4
  17. package/errors/index.js +4 -0
  18. package/errors/mapping-already-exists-error.js +14 -0
  19. package/generated/prisma-mongodb/edge.js +38 -33
  20. package/generated/prisma-mongodb/index-browser.js +35 -30
  21. package/generated/prisma-mongodb/index.d.ts +1880 -3191
  22. package/generated/prisma-mongodb/index.js +38 -33
  23. package/generated/prisma-mongodb/package.json +1 -1
  24. package/generated/prisma-mongodb/schema.prisma +50 -86
  25. package/generated/prisma-mongodb/wasm.js +38 -33
  26. package/generated/prisma-postgresql/edge.js +38 -33
  27. package/generated/prisma-postgresql/index-browser.js +35 -30
  28. package/generated/prisma-postgresql/index.d.ts +2050 -3390
  29. package/generated/prisma-postgresql/index.js +38 -33
  30. package/generated/prisma-postgresql/package.json +1 -1
  31. package/generated/prisma-postgresql/schema.prisma +48 -81
  32. package/generated/prisma-postgresql/wasm.js +38 -33
  33. package/handlers/WEBHOOKS.md +29 -0
  34. package/handlers/app-definition-loader.js +19 -128
  35. package/handlers/app-handler-helpers.js +3 -25
  36. package/handlers/backend-utils.js +25 -31
  37. package/handlers/middleware/admin-auth.js +1 -14
  38. package/handlers/routers/auth.js +10 -46
  39. package/handlers/routers/health.js +4 -13
  40. package/handlers/routers/integration-defined-routers.js +92 -144
  41. package/handlers/routers/integration-webhook-routers.js +64 -102
  42. package/handlers/workers/integration-defined-workers.js +2 -5
  43. package/index.js +3 -65
  44. package/infrastructure/scheduler/index.js +6 -19
  45. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  46. package/integrations/data-defined/README.md +169 -0
  47. package/integrations/data-defined/canonical.js +11 -0
  48. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  49. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  50. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  51. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  52. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  53. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  54. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  55. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  56. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  57. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  58. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  59. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  60. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  61. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  62. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  63. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  64. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  65. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  66. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  67. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  68. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  69. package/integrations/data-defined/compile.js +77 -0
  70. package/integrations/data-defined/config-form.js +110 -0
  71. package/integrations/data-defined/data-defined-integration.js +366 -0
  72. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  73. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  74. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  75. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  76. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  77. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  78. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  79. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  80. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  81. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  82. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  83. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  84. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  87. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  88. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  89. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  90. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  91. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  92. package/integrations/data-defined/expressions.js +225 -0
  93. package/integrations/data-defined/index.js +39 -0
  94. package/integrations/data-defined/interpreter.js +89 -0
  95. package/integrations/data-defined/manifests.js +125 -0
  96. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  97. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  98. package/integrations/data-defined/router.js +97 -0
  99. package/integrations/data-defined/runtime.js +208 -0
  100. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  101. package/integrations/data-defined/schema/installation.v0.1.schema.json +83 -0
  102. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +848 -0
  103. package/integrations/data-defined/schema/records.md +98 -0
  104. package/integrations/data-defined/schema-check.js +58 -0
  105. package/integrations/data-defined/scripts/validate.js +66 -0
  106. package/integrations/data-defined/steps.js +89 -0
  107. package/integrations/data-defined/transforms.js +91 -0
  108. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  109. package/integrations/data-defined/validate.js +370 -0
  110. package/integrations/data-defined/versioning.js +161 -0
  111. package/integrations/data-defined/webhooks.js +119 -0
  112. package/integrations/index.js +0 -1
  113. package/integrations/integration-base.js +29 -0
  114. package/integrations/integration-router.js +101 -131
  115. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  116. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  117. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  118. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  119. package/integrations/repositories/integration-mapping-repository.js +29 -0
  120. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  121. package/integrations/repositories/integration-repository-interface.js +1 -17
  122. package/integrations/repositories/integration-repository-mongo.js +77 -60
  123. package/integrations/repositories/integration-repository-postgres.js +76 -62
  124. package/integrations/repositories/process-repository-mongo.js +1 -1
  125. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  126. package/integrations/use-cases/create-integration.js +20 -55
  127. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  128. package/integrations/use-cases/index.js +1 -7
  129. package/integrations/utils/map-integration-dto.js +14 -43
  130. package/modules/index.js +0 -14
  131. package/modules/module-factory.js +0 -12
  132. package/modules/module.js +2 -53
  133. package/modules/repositories/module-repository-documentdb.js +6 -51
  134. package/modules/repositories/module-repository-interface.js +2 -19
  135. package/modules/repositories/module-repository-mongo.js +74 -67
  136. package/modules/repositories/module-repository-postgres.js +74 -66
  137. package/modules/repositories/module-repository.js +1 -2
  138. package/modules/requester/requester.js +5 -35
  139. package/modules/use-cases/get-entities-for-user.js +13 -26
  140. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  141. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  142. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  143. package/modules/use-cases/get-module.js +4 -10
  144. package/modules/use-cases/process-authorization-callback.js +19 -53
  145. package/modules/use-cases/refresh-entity-options.js +4 -10
  146. package/modules/use-cases/test-module-auth.js +4 -10
  147. package/package.json +6 -6
  148. package/prisma-mongodb/schema.prisma +50 -86
  149. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  150. package/prisma-postgresql/schema.prisma +48 -81
  151. package/queues/index.js +0 -25
  152. package/types/index.d.ts +0 -2
  153. package/user/repositories/user-repository-documentdb.js +2 -27
  154. package/user/repositories/user-repository-interface.js +0 -18
  155. package/user/repositories/user-repository-mongo.js +0 -24
  156. package/user/repositories/user-repository-postgres.js +0 -18
  157. package/user/use-cases/authenticate-user.js +1 -7
  158. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  159. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  160. package/user/user.js +1 -64
  161. package/credential/use-cases/credential-operations-for-user.js +0 -202
  162. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  163. package/database/organization-where.js +0 -18
  164. package/database/unique-violation.js +0 -25
  165. package/extensions/bootstrap-runner.js +0 -43
  166. package/extensions/extension-loader.js +0 -187
  167. package/extensions/index.js +0 -41
  168. package/extensions/initialize-app.js +0 -123
  169. package/extensions/route-mounter.js +0 -103
  170. package/extensions/schema-composer.js +0 -188
  171. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  172. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  173. package/integrations/repositories/integration-not-found.js +0 -14
  174. package/integrations/schedule-config.js +0 -80
  175. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  176. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  177. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  178. package/management-api/create-management-api-router.js +0 -49
  179. package/management-api/error-response.js +0 -116
  180. package/management-api/management-api-config.js +0 -151
  181. package/management-api/meta-router.js +0 -76
  182. package/management-api/openapi/build-openapi.js +0 -144
  183. package/management-api/openapi/components.js +0 -415
  184. package/management-api/openapi/v1-operations.js +0 -83
  185. package/management-api/openapi/v2-operations.js +0 -264
  186. package/management-api/route-registry.js +0 -411
  187. package/management-api/test-support/http-client.js +0 -39
  188. package/management-api/use-cases/get-management-api-meta.js +0 -57
  189. package/management-api/v1-deprecation.js +0 -120
  190. package/management-api/v2/create-v2-router.js +0 -85
  191. package/management-api/v2/dependencies.js +0 -281
  192. package/management-api/v2/dto.js +0 -143
  193. package/management-api/v2/handlers/authorization.js +0 -94
  194. package/management-api/v2/handlers/credentials.js +0 -33
  195. package/management-api/v2/handlers/entities.js +0 -77
  196. package/management-api/v2/handlers/integrations.js +0 -128
  197. package/management-api/v2/handlers/proxy.js +0 -34
  198. package/management-api/v2/request.js +0 -85
  199. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  200. package/modules/domain/authorization-session.js +0 -144
  201. package/modules/entity-scope.js +0 -15
  202. package/modules/entity-type-registry.js +0 -311
  203. package/modules/proxy/proxy-bases.js +0 -169
  204. package/modules/proxy/proxy-errors.js +0 -31
  205. package/modules/proxy/proxy-path.js +0 -183
  206. package/modules/proxy/proxy-policy.js +0 -277
  207. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  208. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  209. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  210. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  211. package/modules/repositories/entity-not-found.js +0 -18
  212. package/modules/requester/proxy-request.js +0 -245
  213. package/modules/scope-config.js +0 -192
  214. package/modules/scope-policy.js +0 -112
  215. package/modules/scope-resolution.js +0 -288
  216. package/modules/scope-visibility.js +0 -247
  217. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  218. package/modules/tests/doubles/ownership-matching.js +0 -18
  219. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  220. package/modules/use-cases/create-entity-from-credential.js +0 -113
  221. package/modules/use-cases/entity-operations-for-user.js +0 -162
  222. package/modules/use-cases/entity-types.js +0 -139
  223. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  224. package/modules/use-cases/get-authorization-requirements.js +0 -100
  225. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  226. package/modules/use-cases/manage-access.js +0 -21
  227. package/modules/use-cases/multi-step-authorization.js +0 -329
  228. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  229. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  230. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  231. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  232. package/providers/index.js +0 -23
  233. package/providers/resolve-provider.js +0 -93
  234. package/providers/runtime-detection.js +0 -124
  235. package/queues/providers/index.js +0 -11
  236. package/queues/providers/netlify-background-provider.js +0 -150
  237. package/queues/providers/qstash-queue-provider.js +0 -219
  238. package/queues/providers/sqs-queue-provider.js +0 -70
  239. package/queues/queue-client-interface.js +0 -55
  240. package/queues/queue-message-auth.js +0 -193
  241. package/queues/queue-provider-factory.js +0 -101
  242. package/queues/queue-provider.js +0 -65
  243. package/queues/sqs-queue-client.js +0 -81
  244. package/types/modules/index.d.ts +0 -214
  245. package/types/user/index.d.ts +0 -134
  246. package/user/adopter-jwt-config.js +0 -177
  247. package/user/adopter-jwt-errors.js +0 -59
  248. package/user/adopter-token-verifier.js +0 -122
  249. package/user/app-user-helpers.js +0 -97
  250. package/user/create-user-authentication.js +0 -64
  251. package/user/membership.js +0 -51
  252. package/user/repositories/organization-membership-record.js +0 -19
  253. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  254. package/user/repositories/organization-membership-repository-factory.js +0 -42
  255. package/user/repositories/organization-membership-repository-interface.js +0 -57
  256. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  257. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  258. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  259. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  260. package/user/use-cases/ensure-organization-membership.js +0 -92
  261. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,118 +0,0 @@
1
- /**
2
- * Scheduled Job Repository
3
- *
4
- * Prisma-based implementation of the repository interface required by
5
- * NetlifySchedulerAdapter. Persists one-time scheduled jobs to the
6
- * ScheduledJob model so they survive function restarts.
7
- *
8
- * Repository interface:
9
- * save(scheduleData) — Upsert a scheduled job
10
- * delete(scheduleName) — Remove a scheduled job
11
- * findByName(scheduleName) — Find a single scheduled job
12
- * findDue(now, { leaseMs }) — Find due jobs: PENDING, or PROCESSING with an
13
- * expired lease (a dispatcher that died after
14
- * claiming), where scheduledAt <= now
15
- * claim(scheduleName, now, { leaseMs })
16
- * — Atomically move one due job to PROCESSING;
17
- * true only for the one caller that won it
18
- *
19
- * The lease clock is the row's updatedAt: a claim writes it, so a PROCESSING
20
- * row whose updatedAt is older than now - leaseMs is claimable again.
21
- */
22
-
23
- const DEFAULT_LEASE_MS = 5 * 60 * 1000;
24
-
25
- // Rows a dispatcher may take: never claimed, or claimed by one that has not
26
- // finished within the lease.
27
- function claimableWhere(now, leaseMs = DEFAULT_LEASE_MS) {
28
- return {
29
- OR: [
30
- { state: 'PENDING' },
31
- {
32
- state: 'PROCESSING',
33
- updatedAt: { lt: new Date(now.getTime() - leaseMs) },
34
- },
35
- ],
36
- };
37
- }
38
-
39
- class ScheduledJobRepository {
40
- /**
41
- * @param {Object} options
42
- * @param {import('@prisma/client').PrismaClient} options.prismaClient
43
- */
44
- constructor({ prismaClient }) {
45
- if (!prismaClient) {
46
- throw new Error(
47
- 'ScheduledJobRepository requires a prismaClient instance'
48
- );
49
- }
50
- this.prisma = prismaClient;
51
- }
52
-
53
- async save(scheduleData) {
54
- return this.prisma.scheduledJob.upsert({
55
- where: { scheduleName: scheduleData.scheduleName },
56
- update: {
57
- scheduledAt: new Date(scheduleData.scheduledAt),
58
- queueResourceId: scheduleData.queueResourceId,
59
- payload: scheduleData.payload ?? undefined,
60
- state: scheduleData.state || 'PENDING',
61
- },
62
- create: {
63
- scheduleName: scheduleData.scheduleName,
64
- scheduledAt: new Date(scheduleData.scheduledAt),
65
- queueResourceId: scheduleData.queueResourceId,
66
- payload: scheduleData.payload ?? undefined,
67
- state: scheduleData.state || 'PENDING',
68
- },
69
- });
70
- }
71
-
72
- async delete(scheduleName) {
73
- try {
74
- await this.prisma.scheduledJob.delete({
75
- where: { scheduleName },
76
- });
77
- } catch (error) {
78
- // P2025 = record not found — safe to ignore on delete
79
- if (error.code !== 'P2025') {
80
- throw error;
81
- }
82
- }
83
- }
84
-
85
- async findByName(scheduleName) {
86
- return this.prisma.scheduledJob.findUnique({
87
- where: { scheduleName },
88
- });
89
- }
90
-
91
- async findDue(now, { leaseMs = DEFAULT_LEASE_MS } = {}) {
92
- return this.prisma.scheduledJob.findMany({
93
- where: {
94
- scheduledAt: { lte: now },
95
- ...claimableWhere(now, leaseMs),
96
- },
97
- orderBy: { scheduledAt: 'asc' },
98
- });
99
- }
100
-
101
- /**
102
- * Conditional update: only a row that is still claimable moves to
103
- * PROCESSING, so of two overlapping ticks exactly one gets count === 1.
104
- * (PostgreSQL re-checks the WHERE on the row it locked; MongoDB updates
105
- * each document atomically.)
106
- *
107
- * @returns {Promise<boolean>} true when this caller claimed the job
108
- */
109
- async claim(scheduleName, now, { leaseMs = DEFAULT_LEASE_MS } = {}) {
110
- const { count } = await this.prisma.scheduledJob.updateMany({
111
- where: { scheduleName, ...claimableWhere(now, leaseMs) },
112
- data: { state: 'PROCESSING', updatedAt: now },
113
- });
114
- return count === 1;
115
- }
116
- }
117
-
118
- module.exports = { ScheduledJobRepository, DEFAULT_LEASE_MS };
@@ -1,14 +0,0 @@
1
- /**
2
- * The error `findIntegrationById` throws for an id with no row, and the one
3
- * predicate that recognises it. The adapters build the error here and the
4
- * use cases match it here, so the message is written once and a missing
5
- * integration can be told from any other failure without widening a regex.
6
- */
7
- const MESSAGE = /^Integration with id .+ not found$/;
8
-
9
- const integrationNotFoundError = (id) =>
10
- new Error(`Integration with id ${id} not found`);
11
-
12
- const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
13
-
14
- module.exports = { integrationNotFoundError, isIntegrationNotFound };
@@ -1,80 +0,0 @@
1
- /**
2
- * An integration's schedule configuration (`static Definition.schedule`),
3
- * read by hosts that tick ONGOING_SYNC from a cron (provider-netlify's
4
- * scheduled-sync function).
5
- *
6
- * schedule: {
7
- * // 'class' (default): one ONGOING_SYNC per integration class per tick,
8
- * // with no integration id: the worker runs it on an unhydrated
9
- * // instance, so the handler finds its own installations.
10
- * // 'installation': one ONGOING_SYNC per ENABLED integration record of
11
- * // the class, carrying `data.integrationId`, so the worker hydrates
12
- * // that installation before the handler runs.
13
- * scope: 'installation',
14
- * // Optional, 'installation' only: (integration, { now }) => boolean |
15
- * // Promise<boolean>. Return false to skip that installation this tick.
16
- * // Without it every installation is dispatched on every tick.
17
- * isDue: (integration, { now }) => true,
18
- * }
19
- *
20
- * Validated at load (loadAppDefinition) so a mistyped scope stops the app
21
- * rather than silently falling back to 'class'.
22
- */
23
-
24
- const SCHEDULE_SCOPES = Object.freeze({
25
- CLASS: 'class',
26
- INSTALLATION: 'installation',
27
- });
28
-
29
- const DEFAULT_SCHEDULE_SCOPE = SCHEDULE_SCOPES.CLASS;
30
-
31
- class ScheduleConfigError extends Error {
32
- constructor(message) {
33
- super(message);
34
- this.name = 'ScheduleConfigError';
35
- this.code = 'SCHEDULE_CONFIG_INVALID';
36
- }
37
- }
38
-
39
- /**
40
- * @param {Object} [definition] - an integration's static Definition
41
- * @returns {{ scope: 'class'|'installation', isDue: Function|null }}
42
- * @throws {ScheduleConfigError} when `schedule` is malformed
43
- */
44
- function resolveScheduleConfig(definition) {
45
- const name = definition?.name || 'integration';
46
- const schedule = definition?.schedule;
47
- if (schedule === undefined || schedule === null) {
48
- return { scope: DEFAULT_SCHEDULE_SCOPE, isDue: null };
49
- }
50
- if (typeof schedule !== 'object' || Array.isArray(schedule)) {
51
- throw new ScheduleConfigError(`${name}: Definition.schedule must be an object`);
52
- }
53
-
54
- const scopes = Object.values(SCHEDULE_SCOPES);
55
- const scope = schedule.scope === undefined ? DEFAULT_SCHEDULE_SCOPE : schedule.scope;
56
- if (!scopes.includes(scope)) {
57
- throw new ScheduleConfigError(
58
- `${name}: Definition.schedule.scope must be one of ${scopes.map((s) => `'${s}'`).join(', ')} (got ${JSON.stringify(schedule.scope)})`
59
- );
60
- }
61
-
62
- const isDue = schedule.isDue === undefined ? null : schedule.isDue;
63
- if (isDue !== null && typeof isDue !== 'function') {
64
- throw new ScheduleConfigError(`${name}: Definition.schedule.isDue must be a function`);
65
- }
66
- if (isDue && scope !== SCHEDULE_SCOPES.INSTALLATION) {
67
- throw new ScheduleConfigError(
68
- `${name}: Definition.schedule.isDue applies to scope 'installation' only`
69
- );
70
- }
71
-
72
- return { scope, isDue };
73
- }
74
-
75
- module.exports = {
76
- SCHEDULE_SCOPES,
77
- DEFAULT_SCHEDULE_SCOPE,
78
- ScheduleConfigError,
79
- resolveScheduleConfig,
80
- };
@@ -1,100 +0,0 @@
1
- const {
2
- SCHEDULE_SCOPES,
3
- resolveScheduleConfig,
4
- } = require('../schedule-config');
5
-
6
- const ONGOING_SYNC_EVENT = 'ONGOING_SYNC';
7
- const ACTIVE_STATUS = 'ENABLED';
8
-
9
- /**
10
- * Dispatches one cron tick of ONGOING_SYNC to the queue worker, per each
11
- * integration class's `Definition.schedule.scope` (schedule-config.js):
12
- *
13
- * - 'class' (default): one message per class, `data: {}`.
14
- * - 'installation': one message per ENABLED integration record of the class,
15
- * `data: { integrationId }`, so the worker hydrates that installation;
16
- * skipped when the definition's `isDue(integration, { now })` says no.
17
- *
18
- * Only classes that implement `startOngoingSync` take part. A failure for one
19
- * class or installation is counted and logged; the rest still go out.
20
- */
21
- class DispatchOngoingSyncs {
22
- /**
23
- * @param {Object} params
24
- * @param {Object} params.integrationRepository - findIntegrations({ type, status })
25
- * @param {Object} params.queueProvider - send(message, queueId)
26
- * @param {string} params.queueId - the worker the messages go to
27
- * @param {() => Date} [params.now]
28
- * @param {Object} [params.logger=console]
29
- */
30
- constructor({ integrationRepository, queueProvider, queueId, now = () => new Date(), logger = console }) {
31
- this.integrationRepository = integrationRepository;
32
- this.queueProvider = queueProvider;
33
- this.queueId = queueId;
34
- this.now = now;
35
- this.logger = logger;
36
- }
37
-
38
- /**
39
- * @param {Array<Function>} integrationClasses
40
- * @returns {Promise<{ dispatched: number, notDue: number, errors: number }>}
41
- */
42
- async execute(integrationClasses = []) {
43
- const result = { dispatched: 0, notDue: 0, errors: 0 };
44
- const now = this.now();
45
-
46
- for (const IntegrationClass of integrationClasses) {
47
- const name = IntegrationClass?.Definition?.name;
48
- if (!name) continue;
49
- if (typeof IntegrationClass.prototype?.startOngoingSync !== 'function') continue;
50
-
51
- try {
52
- const { scope, isDue } = resolveScheduleConfig(IntegrationClass.Definition);
53
- if (scope === SCHEDULE_SCOPES.INSTALLATION) {
54
- await this._dispatchPerInstallation(name, isDue, now, result);
55
- } else {
56
- await this._send(name, {});
57
- result.dispatched++;
58
- }
59
- } catch (error) {
60
- result.errors++;
61
- this.logger.error(`[ScheduledSync] Failed to dispatch sync for ${name}:`, error.message);
62
- }
63
- }
64
-
65
- return result;
66
- }
67
-
68
- async _dispatchPerInstallation(name, isDue, now, result) {
69
- const integrations = await this.integrationRepository.findIntegrations({
70
- type: name,
71
- status: ACTIVE_STATUS,
72
- });
73
-
74
- for (const integration of integrations || []) {
75
- try {
76
- if (isDue && (await isDue(integration, { now })) !== true) {
77
- result.notDue++;
78
- continue;
79
- }
80
- await this._send(name, { integrationId: integration.id });
81
- result.dispatched++;
82
- } catch (error) {
83
- result.errors++;
84
- this.logger.error(
85
- `[ScheduledSync] Failed to dispatch sync for ${name} integration ${integration?.id}:`,
86
- error.message
87
- );
88
- }
89
- }
90
- }
91
-
92
- _send(integrationName, data) {
93
- return this.queueProvider.send(
94
- { integrationName, event: ONGOING_SYNC_EVENT, data },
95
- this.queueId
96
- );
97
- }
98
- }
99
-
100
- module.exports = { DispatchOngoingSyncs, ONGOING_SYNC_EVENT };
@@ -1,256 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { describeRecord } = require('../../modules/scope-visibility');
3
- const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
4
- const { assertCanManage } = require('../../modules/use-cases/manage-access');
5
- const { isIntegrationNotFound } = require('../repositories/integration-not-found');
6
-
7
- /**
8
- * Management API v2 integration operations. They put an access check
9
- * (missing or unavailable integration = 404) in front of the existing
10
- * integration use cases, and pass the record's owner id on. Access follows
11
- * the owner and the organization the integration was created in (ADR-056
12
- * §3.6), not the owner alone.
13
- */
14
-
15
- const notFound = (integrationId) =>
16
- Boom.notFound(`Integration ${integrationId} not found`, {
17
- code: 'INTEGRATION_NOT_FOUND',
18
- });
19
-
20
- /**
21
- * The integration record, or 404 when it is missing or the caller may not
22
- * use it: not theirs or their active organization's, or created in another
23
- * organization.
24
- */
25
- class GetOwnedIntegrationRecord {
26
- constructor({ integrationRepository, scopePolicy }) {
27
- this.integrationRepository = integrationRepository;
28
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
29
- }
30
-
31
- /** `{ scope, owner, organizationId, access }` of a record to this caller, or null. */
32
- describe(record, user) {
33
- return describeRecord(record, this.scopePolicy.accessContext(user), 'integration');
34
- }
35
-
36
- /**
37
- * @param {string} integrationId
38
- * @param {Object} user - authenticated User
39
- * @param {{manage?: boolean}} [options] - `manage: true` for changing or
40
- * deleting: a person may use an organization's integration but only an
41
- * admin may change it (403 ORG_ADMIN_REQUIRED)
42
- */
43
- async execute(integrationId, user, { manage = false } = {}) {
44
- let record = null;
45
- try {
46
- record = await this.integrationRepository.findIntegrationById(
47
- integrationId
48
- );
49
- } catch (error) {
50
- if (
51
- /Invalid ID|ObjectId|Malformed/i.test(error?.message || '') ||
52
- isIntegrationNotFound(error)
53
- ) {
54
- throw notFound(integrationId);
55
- }
56
- throw error;
57
- }
58
- const tenancy = record && this.describe(record, user);
59
- if (!tenancy) throw notFound(integrationId);
60
- if (manage) assertCanManage(tenancy.access, `integration ${integrationId}`);
61
- return record;
62
- }
63
- }
64
-
65
- /**
66
- * Creates (or reuses) an integration between entities the caller may use.
67
- * The entities' access, the binding scopes and the integration type are
68
- * checked before anything is written, and the integration is owned and
69
- * paired with an organization by `resolveIntegrationOwnership` (§3, §3.6).
70
- */
71
- class CreateIntegrationForUser {
72
- constructor({ createIntegration, resolveIntegrationOwnership, integrationClasses }) {
73
- this.createIntegration = createIntegration;
74
- this.resolveIntegrationOwnership = resolveIntegrationOwnership;
75
- this.integrationClasses = integrationClasses || [];
76
- }
77
-
78
- async execute({ entities, config }, user) {
79
- if (!config || typeof config !== 'object' || typeof config.type !== 'string') {
80
- throw Boom.badRequest('config.type is required', {
81
- code: 'VALIDATION_ERROR',
82
- details: { field: 'config.type' },
83
- });
84
- }
85
- const known = this.integrationClasses.some(
86
- (IntegrationClass) => IntegrationClass.Definition?.name === config.type
87
- );
88
- if (!known) {
89
- throw Boom.badRequest(`Unknown integration type '${config.type}'`, {
90
- code: 'UNKNOWN_INTEGRATION_TYPE',
91
- });
92
- }
93
- if (
94
- !Array.isArray(entities) ||
95
- entities.some((id) => typeof id !== 'string' && typeof id !== 'number')
96
- ) {
97
- throw Boom.badRequest('entities must be an array of entity ids', {
98
- code: 'VALIDATION_ERROR',
99
- details: { field: 'entities' },
100
- });
101
- }
102
- const { ownerId, organizationId, adoptUnscoped } =
103
- await this.resolveIntegrationOwnership.execute({
104
- entityIds: entities,
105
- config,
106
- user,
107
- });
108
- return this.createIntegration.execute(
109
- entities.map(String),
110
- ownerId,
111
- config,
112
- { organizationId, adoptUnscoped }
113
- );
114
- }
115
- }
116
-
117
- /** The hydrated integration instance (modules loaded) for an owned integration. */
118
- class GetIntegrationInstanceForUser {
119
- constructor({ getOwnedIntegrationRecord, getIntegrationInstance }) {
120
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
121
- this.getIntegrationInstance = getIntegrationInstance;
122
- }
123
-
124
- async execute(integrationId, user) {
125
- const record = await this.getOwnedIntegrationRecord.execute(
126
- integrationId,
127
- user
128
- );
129
- return this.getIntegrationInstance.execute(record.id, record.userId);
130
- }
131
- }
132
-
133
- /** Replaces an owned integration's config. */
134
- class UpdateIntegrationForUser {
135
- constructor({ getOwnedIntegrationRecord, updateIntegration }) {
136
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
137
- this.updateIntegration = updateIntegration;
138
- }
139
-
140
- async execute(integrationId, user, config) {
141
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
142
- throw Boom.badRequest('config must be an object', {
143
- code: 'VALIDATION_ERROR',
144
- details: { field: 'config' },
145
- });
146
- }
147
- const record = await this.getOwnedIntegrationRecord.execute(
148
- integrationId,
149
- user,
150
- { manage: true }
151
- );
152
- return this.updateIntegration.execute(record.id, record.userId, config);
153
- }
154
- }
155
-
156
- /** Deletes an owned integration (runs its onDelete hooks). */
157
- class RemoveIntegrationForUser {
158
- constructor({ getOwnedIntegrationRecord, deleteIntegrationForUser }) {
159
- this.getOwnedIntegrationRecord = getOwnedIntegrationRecord;
160
- this.deleteIntegrationForUser = deleteIntegrationForUser;
161
- }
162
-
163
- async execute(integrationId, user) {
164
- const record = await this.getOwnedIntegrationRecord.execute(
165
- integrationId,
166
- user,
167
- { manage: true }
168
- );
169
- await this.deleteIntegrationForUser.execute(record.id, record.userId);
170
- }
171
- }
172
-
173
- /**
174
- * Sends one of the framework's user-facing events (GET_CONFIG_OPTIONS,
175
- * GET_USER_ACTIONS, ...) to an owned integration. Handlers pass fixed event
176
- * names only; caller-chosen action ids go through RunIntegrationActionForUser.
177
- */
178
- class SendIntegrationEventForUser {
179
- constructor({ getIntegrationInstanceForUser }) {
180
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
181
- }
182
-
183
- async execute(integrationId, user, event, payload) {
184
- const instance = await this.getIntegrationInstanceForUser.execute(
185
- integrationId,
186
- user
187
- );
188
- return instance.send(event, payload);
189
- }
190
- }
191
-
192
- /**
193
- * Runs a user action on an owned integration. Unlike v1, the action id must
194
- * be one the integration lists as a user action (static USER_ACTION events
195
- * or its dynamic user actions), so a caller cannot fire internal events such
196
- * as lifecycle hooks.
197
- */
198
- class RunIntegrationActionForUser {
199
- constructor({ getIntegrationInstanceForUser }) {
200
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
201
- }
202
-
203
- async execute(integrationId, user, actionId, payload) {
204
- const instance = await this.getIntegrationInstanceForUser.execute(
205
- integrationId,
206
- user
207
- );
208
- const actions =
209
- typeof instance.loadUserActions === 'function'
210
- ? await instance.loadUserActions()
211
- : {};
212
- if (!actions || !Object.prototype.hasOwnProperty.call(actions, actionId)) {
213
- throw Boom.notFound(
214
- `Integration ${integrationId} has no user action '${actionId}'`,
215
- { code: 'ACTION_NOT_FOUND' }
216
- );
217
- }
218
- return instance.send(actionId, payload);
219
- }
220
- }
221
-
222
- /**
223
- * Runs the integration's auth test and reconciles its status. Returns the
224
- * errors recorded during the test.
225
- */
226
- class TestIntegrationAuthForUser {
227
- constructor({ getIntegrationInstanceForUser }) {
228
- this.getIntegrationInstanceForUser = getIntegrationInstanceForUser;
229
- }
230
-
231
- /** @returns {Promise<{ ok: boolean, errors: Array<Object> }>} */
232
- async execute(integrationId, user) {
233
- const instance = await this.getIntegrationInstanceForUser.execute(
234
- integrationId,
235
- user
236
- );
237
- const start = Date.now();
238
- const authPassed = await instance.testAuth();
239
- await instance.reconcileAuthStatus(authPassed);
240
- const errors = (instance.record?.messages?.errors || []).filter(
241
- ({ timestamp }) => timestamp >= start
242
- );
243
- return { ok: Boolean(authPassed) && errors.length === 0, errors };
244
- }
245
- }
246
-
247
- module.exports = {
248
- GetOwnedIntegrationRecord,
249
- CreateIntegrationForUser,
250
- GetIntegrationInstanceForUser,
251
- UpdateIntegrationForUser,
252
- RemoveIntegrationForUser,
253
- SendIntegrationEventForUser,
254
- RunIntegrationActionForUser,
255
- TestIntegrationAuthForUser,
256
- };