@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.694.f66a64a.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 (267) 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 +2 -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/handlers/workers/integration-triggers.js +25 -0
  44. package/index.js +3 -65
  45. package/infrastructure/scheduler/index.js +6 -19
  46. package/infrastructure/scheduler/recurrence.js +204 -0
  47. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  48. package/integrations/data-defined/README.md +185 -0
  49. package/integrations/data-defined/canonical.js +11 -0
  50. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  51. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  52. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  53. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  54. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  55. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  56. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  57. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  58. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  59. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  60. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  61. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  62. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  63. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  64. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  65. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  66. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  67. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  68. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  69. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  70. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  71. package/integrations/data-defined/compile.js +80 -0
  72. package/integrations/data-defined/config-form.js +137 -0
  73. package/integrations/data-defined/data-defined-integration.js +417 -0
  74. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  75. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  76. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  77. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  78. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  79. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  80. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  81. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  82. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  83. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  84. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  87. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  88. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  89. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  90. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  91. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  92. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  93. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  94. package/integrations/data-defined/expressions.js +225 -0
  95. package/integrations/data-defined/index.js +39 -0
  96. package/integrations/data-defined/interpreter.js +89 -0
  97. package/integrations/data-defined/manifests.js +125 -0
  98. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  99. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  100. package/integrations/data-defined/router.js +97 -0
  101. package/integrations/data-defined/runtime.js +208 -0
  102. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  103. package/integrations/data-defined/schema/installation.v0.1.schema.json +130 -0
  104. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +902 -0
  105. package/integrations/data-defined/schema/records.md +98 -0
  106. package/integrations/data-defined/schema-check.js +58 -0
  107. package/integrations/data-defined/scripts/validate.js +66 -0
  108. package/integrations/data-defined/steps.js +89 -0
  109. package/integrations/data-defined/transforms.js +91 -0
  110. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  111. package/integrations/data-defined/validate.js +384 -0
  112. package/integrations/data-defined/versioning.js +176 -0
  113. package/integrations/data-defined/webhooks.js +119 -0
  114. package/integrations/index.js +0 -1
  115. package/integrations/integration-base.js +117 -0
  116. package/integrations/integration-router.js +101 -131
  117. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  118. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  119. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  120. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  121. package/integrations/repositories/integration-mapping-repository.js +29 -0
  122. package/integrations/repositories/integration-repository-documentdb.js +9 -37
  123. package/integrations/repositories/integration-repository-interface.js +4 -17
  124. package/integrations/repositories/integration-repository-mongo.js +96 -64
  125. package/integrations/repositories/integration-repository-postgres.js +81 -63
  126. package/integrations/repositories/process-repository-mongo.js +1 -1
  127. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  128. package/integrations/triggers.js +244 -0
  129. package/integrations/use-cases/create-integration.js +20 -55
  130. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  131. package/integrations/use-cases/index.js +1 -7
  132. package/integrations/use-cases/run-trigger-tick.js +132 -0
  133. package/integrations/use-cases/run-trigger.js +77 -0
  134. package/integrations/utils/map-integration-dto.js +14 -43
  135. package/modules/index.js +0 -14
  136. package/modules/module-factory.js +0 -12
  137. package/modules/module.js +2 -53
  138. package/modules/repositories/module-repository-documentdb.js +6 -51
  139. package/modules/repositories/module-repository-interface.js +2 -19
  140. package/modules/repositories/module-repository-mongo.js +74 -67
  141. package/modules/repositories/module-repository-postgres.js +74 -66
  142. package/modules/repositories/module-repository.js +1 -2
  143. package/modules/requester/requester.js +5 -35
  144. package/modules/use-cases/get-entities-for-user.js +13 -26
  145. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  146. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  147. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  148. package/modules/use-cases/get-module.js +4 -10
  149. package/modules/use-cases/process-authorization-callback.js +19 -53
  150. package/modules/use-cases/refresh-entity-options.js +4 -10
  151. package/modules/use-cases/test-module-auth.js +4 -10
  152. package/package.json +6 -6
  153. package/prisma-mongodb/schema.prisma +50 -86
  154. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  155. package/prisma-postgresql/schema.prisma +48 -81
  156. package/queues/index.js +0 -25
  157. package/queues/queuer-util.js +9 -2
  158. package/types/index.d.ts +0 -2
  159. package/user/repositories/user-repository-documentdb.js +2 -27
  160. package/user/repositories/user-repository-interface.js +0 -18
  161. package/user/repositories/user-repository-mongo.js +0 -24
  162. package/user/repositories/user-repository-postgres.js +0 -18
  163. package/user/use-cases/authenticate-user.js +1 -7
  164. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  165. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  166. package/user/user.js +1 -64
  167. package/credential/use-cases/credential-operations-for-user.js +0 -202
  168. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  169. package/database/organization-where.js +0 -18
  170. package/database/unique-violation.js +0 -25
  171. package/extensions/bootstrap-runner.js +0 -43
  172. package/extensions/extension-loader.js +0 -187
  173. package/extensions/index.js +0 -41
  174. package/extensions/initialize-app.js +0 -123
  175. package/extensions/route-mounter.js +0 -103
  176. package/extensions/schema-composer.js +0 -188
  177. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  178. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  179. package/integrations/repositories/integration-not-found.js +0 -14
  180. package/integrations/schedule-config.js +0 -80
  181. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  182. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  183. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  184. package/management-api/create-management-api-router.js +0 -49
  185. package/management-api/error-response.js +0 -116
  186. package/management-api/management-api-config.js +0 -151
  187. package/management-api/meta-router.js +0 -76
  188. package/management-api/openapi/build-openapi.js +0 -144
  189. package/management-api/openapi/components.js +0 -415
  190. package/management-api/openapi/v1-operations.js +0 -83
  191. package/management-api/openapi/v2-operations.js +0 -264
  192. package/management-api/route-registry.js +0 -411
  193. package/management-api/test-support/http-client.js +0 -39
  194. package/management-api/use-cases/get-management-api-meta.js +0 -57
  195. package/management-api/v1-deprecation.js +0 -120
  196. package/management-api/v2/create-v2-router.js +0 -85
  197. package/management-api/v2/dependencies.js +0 -281
  198. package/management-api/v2/dto.js +0 -143
  199. package/management-api/v2/handlers/authorization.js +0 -94
  200. package/management-api/v2/handlers/credentials.js +0 -33
  201. package/management-api/v2/handlers/entities.js +0 -77
  202. package/management-api/v2/handlers/integrations.js +0 -128
  203. package/management-api/v2/handlers/proxy.js +0 -34
  204. package/management-api/v2/request.js +0 -85
  205. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  206. package/modules/domain/authorization-session.js +0 -144
  207. package/modules/entity-scope.js +0 -15
  208. package/modules/entity-type-registry.js +0 -311
  209. package/modules/proxy/proxy-bases.js +0 -169
  210. package/modules/proxy/proxy-errors.js +0 -31
  211. package/modules/proxy/proxy-path.js +0 -183
  212. package/modules/proxy/proxy-policy.js +0 -277
  213. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  214. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  215. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  216. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  217. package/modules/repositories/entity-not-found.js +0 -18
  218. package/modules/requester/proxy-request.js +0 -245
  219. package/modules/scope-config.js +0 -192
  220. package/modules/scope-policy.js +0 -112
  221. package/modules/scope-resolution.js +0 -288
  222. package/modules/scope-visibility.js +0 -247
  223. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  224. package/modules/tests/doubles/ownership-matching.js +0 -18
  225. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  226. package/modules/use-cases/create-entity-from-credential.js +0 -113
  227. package/modules/use-cases/entity-operations-for-user.js +0 -162
  228. package/modules/use-cases/entity-types.js +0 -139
  229. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  230. package/modules/use-cases/get-authorization-requirements.js +0 -100
  231. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  232. package/modules/use-cases/manage-access.js +0 -21
  233. package/modules/use-cases/multi-step-authorization.js +0 -329
  234. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  235. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  236. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  237. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  238. package/providers/index.js +0 -23
  239. package/providers/resolve-provider.js +0 -93
  240. package/providers/runtime-detection.js +0 -124
  241. package/queues/providers/index.js +0 -11
  242. package/queues/providers/netlify-background-provider.js +0 -150
  243. package/queues/providers/qstash-queue-provider.js +0 -219
  244. package/queues/providers/sqs-queue-provider.js +0 -70
  245. package/queues/queue-client-interface.js +0 -55
  246. package/queues/queue-message-auth.js +0 -193
  247. package/queues/queue-provider-factory.js +0 -101
  248. package/queues/queue-provider.js +0 -65
  249. package/queues/sqs-queue-client.js +0 -81
  250. package/types/modules/index.d.ts +0 -214
  251. package/types/user/index.d.ts +0 -134
  252. package/user/adopter-jwt-config.js +0 -177
  253. package/user/adopter-jwt-errors.js +0 -59
  254. package/user/adopter-token-verifier.js +0 -122
  255. package/user/app-user-helpers.js +0 -97
  256. package/user/create-user-authentication.js +0 -64
  257. package/user/membership.js +0 -51
  258. package/user/repositories/organization-membership-record.js +0 -19
  259. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  260. package/user/repositories/organization-membership-repository-factory.js +0 -42
  261. package/user/repositories/organization-membership-repository-interface.js +0 -57
  262. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  263. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  264. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  265. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  266. package/user/use-cases/ensure-organization-membership.js +0 -92
  267. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The trigger tick (ADR-055 "Runtime / Tick"): one invocation of an integration's `{integration}Triggers` function.
3
+ *
4
+ * It is invoked by one of the integration's schedules: `{ tick: '5m', time }` for the shared tick of its `every` and
5
+ * `cron` triggers, or `{ at: '<trigger name>', time }` for an `at` trigger. `time` is the schedule's own firing time,
6
+ * so the slot it falls in, and therefore every due decision and every `scheduledFor`, is the same if the same tick
7
+ * is delivered twice or carried on by a continuation.
8
+ *
9
+ * 1. Page through the ENABLED installations of this integration, `pageSize` (500) at a time, by id.
10
+ * 2. For each installation and trigger, resolve config, `when` and the cadence (integrations/triggers.js).
11
+ * 3. Due: `every` when `slot mod n == hash(integrationId) mod n`; `cron` when it fired in (previous tick, this tick];
12
+ * `at` always (it is its own schedule).
13
+ * 4. Send `{ event: 'FRIGG_TRIGGER_RUN', data: { integrationId, trigger, scheduledFor } }` to the integration queue in
14
+ * batches, with `DelaySeconds` jitter (a stable 0..min(tick, 900 s) per installation and trigger) for tick runs.
15
+ * 5. Near the Lambda deadline, send `{ event: 'FRIGG_TRIGGER_TICK', data: { tick|at, time, cursor } }` to the same
16
+ * queue and stop; the queue worker carries on from the cursor (IntegrationBase.onTriggerTick).
17
+ *
18
+ * Missed ticks are not replayed: a trigger is a "check now" signal, and handlers keep their own watermark.
19
+ */
20
+ const { parseEvery, lastFiringIn } = require('../../infrastructure/scheduler/recurrence');
21
+ const { TRIGGER_RUN, TRIGGER_TICK, resolveTrigger, dueRun, slotStart, hashId } = require('../triggers');
22
+
23
+ const PAGE_SIZE = 500;
24
+ const DEADLINE_MARGIN_MS = 30000;
25
+ const MAX_DELAY_SECONDS = 900; // SQS DelaySeconds ceiling
26
+
27
+ class RunTriggerTick {
28
+ /**
29
+ * @param {Object} deps
30
+ * @param {Function} deps.integrationClass - the IntegrationBase subclass (Definition.name, triggersFor)
31
+ * @param {Object} deps.integrationRepository - findIntegrations({ type, status, limit, after })
32
+ * @param {Object} deps.queue - { batchSend(entries, queueUrl, { delaySeconds }), send(message, queueUrl) }
33
+ * @param {string} deps.queueUrl
34
+ * @param {Object} [deps.logger]
35
+ * @param {number} [deps.pageSize]
36
+ */
37
+ constructor({ integrationClass, integrationRepository, queue, queueUrl, logger, pageSize = PAGE_SIZE }) {
38
+ this.Integration = integrationClass;
39
+ this.repository = integrationRepository;
40
+ this.queue = queue;
41
+ this.queueUrl = queueUrl;
42
+ this.logger = logger;
43
+ this.pageSize = pageSize;
44
+ }
45
+
46
+ /**
47
+ * @param {Object} params
48
+ * @param {string} [params.tick] - '5m': evaluate the every/cron triggers on this tick
49
+ * @param {string} [params.at] - an `at` trigger's name: run it for every ENABLED installation
50
+ * @param {string} [params.time] - the schedule's firing time (ISO); defaults to now
51
+ * @param {string|number} [params.cursor] - continue after this installation id
52
+ * @param {() => number} [params.remainingMs] - time left in this invocation
53
+ * @returns {Promise<{ installations: number, sent: number, pages: number, continued: boolean, failed: Array }>}
54
+ */
55
+ async execute({ tick, at, time, cursor, remainingMs = () => Infinity }) {
56
+ const name = this.Integration.Definition?.name;
57
+ if (!this.queueUrl) throw new Error(`No queue URL for integration "${name}"; trigger runs have nowhere to go`);
58
+ const tickMinutes = at ? null : parseEvery(tick);
59
+ if (!at && tickMinutes === null) throw new Error(`Trigger tick "${tick}" is not a duration`);
60
+ const timeMs = time && !Number.isNaN(Date.parse(time)) ? Date.parse(time) : Date.now();
61
+ const slotMs = at ? Math.floor(timeMs / 60000) * 60000 : slotStart(timeMs, tickMinutes);
62
+ const firings = new Map(); // one cron answer per recurrence per tick (the window is the same), not per installation
63
+ const firing = (recurrence, from, to) => {
64
+ const key = `${recurrence.cron}|${recurrence.timezone || 'UTC'}`;
65
+ if (!firings.has(key)) firings.set(key, lastFiringIn(recurrence, from, to));
66
+ return firings.get(key);
67
+ };
68
+ const delayOf = (m) => (at ? 0 : hashId(`${m.data.integrationId}:${m.data.trigger}`) % Math.min(tickMinutes * 60, MAX_DELAY_SECONDS));
69
+
70
+ const out = { installations: 0, sent: 0, pages: 0, continued: false, failed: [] };
71
+ let after = cursor;
72
+ for (;;) {
73
+ const rows = await this.repository.findIntegrations({ type: name, status: 'ENABLED', limit: this.pageSize, after });
74
+ out.pages += 1;
75
+ const messages = [];
76
+ for (const row of rows) {
77
+ out.installations += 1;
78
+ try {
79
+ const declarations = await this.Integration.triggersFor({ config: row.config || {}, record: row });
80
+ for (const declaration of declarations) {
81
+ const isAt = declaration.at !== undefined;
82
+ if (at ? declaration.name !== at || !isAt : isAt) continue;
83
+ const resolved = resolveTrigger(declaration.name, declaration, row.config || {});
84
+ if (!at && resolved.tickMinutes !== tickMinutes) continue;
85
+ const due = dueRun(resolved, { integrationId: row.id, slotMs, firing });
86
+ if (due) messages.push({ event: TRIGGER_RUN, data: { integrationId: row.id, trigger: declaration.name, scheduledFor: due.scheduledFor } });
87
+ }
88
+ } catch (error) {
89
+ // One installation's bad config must not stop the others' runs.
90
+ out.failed.push({ integrationId: row.id, error: error.message });
91
+ this.logger?.warn('Trigger tick skipped an installation', { eventName: 'frigg.triggers.installation_failed', integration: name, integrationId: row.id, error });
92
+ }
93
+ }
94
+ if (messages.length) {
95
+ await this.queue.batchSend(messages, this.queueUrl, { delaySeconds: delayOf });
96
+ out.sent += messages.length;
97
+ }
98
+ if (rows.length < this.pageSize) return out;
99
+ after = rows[rows.length - 1].id;
100
+ if (remainingMs() < DEADLINE_MARGIN_MS) {
101
+ await this.queue.send({ event: TRIGGER_TICK, data: { ...(at ? { at } : { tick }), time: new Date(timeMs).toISOString(), cursor: after } }, this.queueUrl);
102
+ out.continued = true;
103
+ return out;
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /** The queue URL Frigg's infrastructure gives an integration: `<NAME>_QUEUE_URL`, dashes as underscores. */
110
+ function queueUrlFor(integrationName) {
111
+ return process.env[`${String(integrationName).toUpperCase().replace(/-/g, '_')}_QUEUE_URL`];
112
+ }
113
+
114
+ /**
115
+ * Run a tick with Frigg's real adapters: the integration repository, the SQS queuer and the integration's queue.
116
+ * Used by the `{integration}Triggers` Lambda and by a continuation on the queue worker.
117
+ */
118
+ async function runTriggerTick(integrationClass, event = {}, context) {
119
+ const { createIntegrationRepository } = require('../repositories/integration-repository-factory');
120
+ const { QueuerUtil } = require('../../queues');
121
+ const { getLogger } = require('../../logs');
122
+ const remainingMs = typeof context?.getRemainingTimeInMillis === 'function' ? () => context.getRemainingTimeInMillis() : undefined;
123
+ return new RunTriggerTick({
124
+ integrationClass,
125
+ integrationRepository: createIntegrationRepository(),
126
+ queue: QueuerUtil,
127
+ queueUrl: queueUrlFor(integrationClass.Definition.name),
128
+ logger: getLogger('frigg.triggers'),
129
+ }).execute({ tick: event.tick, at: event.at, time: event.time, cursor: event.cursor, remainingMs });
130
+ }
131
+
132
+ module.exports = { RunTriggerTick, runTriggerTick, queueUrlFor, PAGE_SIZE };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Run one recurring trigger for one installation (ADR-055 "Run"). The queue worker has already hydrated the
3
+ * installation by `integrationId`; this decides whether the run happens and records it.
4
+ *
5
+ * 1. The installation must be ENABLED; anything else discards the run.
6
+ * 2. With `overlap: 'skip'` (the default) a Process record per (installation, trigger), type `TRIGGER:<name>`, is
7
+ * the lease: a run whose record is not terminal and was touched within 2 x tick is still going, so this run is
8
+ * skipped and acked. Otherwise the record is reused (one row per installation and trigger, not one per run), set
9
+ * RUNNING, and set COMPLETED or ERROR when the handler ends. A failed run is marked ERROR before the error is
10
+ * rethrown, so the worker's retry is not skipped by the run it is retrying. `overlap: 'allow'` keeps no record.
11
+ * 3. Errors go back to the queue worker: its retry, halt and DLQ rules apply as for any queued event.
12
+ */
13
+ const TERMINAL = ['COMPLETED', 'ERROR'];
14
+ const AT_LEASE_MINUTES = 60; // an `at` trigger has no tick; its lease is two hours
15
+
16
+ class RunTrigger {
17
+ /**
18
+ * @param {Object} deps
19
+ * @param {Object} deps.processRepository - findByIntegrationAndType, create, update
20
+ * @param {() => number} [deps.now]
21
+ */
22
+ constructor({ processRepository, now = () => Date.now() }) {
23
+ this.processRepository = processRepository;
24
+ this.now = now;
25
+ }
26
+
27
+ /**
28
+ * @param {Object} params
29
+ * @param {Object} params.integration - hydrated instance: id, userId, status, telemetry
30
+ * @param {Object} params.resolved - resolveTrigger(...) for this installation
31
+ * @param {(payload) => Promise<any>} params.invoke - calls the trigger's handler
32
+ * @param {{ trigger: string, scheduledFor: string }} params.payload
33
+ * @returns {Promise<{ status: 'ran'|'skipped'|'discarded', reason?: string, result?: any }>}
34
+ */
35
+ async execute({ integration, resolved, invoke, payload }) {
36
+ const count = (name) => { try { integration.telemetry?.count(name, 1, { trigger: payload.trigger }); } catch (_) { /* never fails a run */ } };
37
+ if (integration.status !== 'ENABLED') return { status: 'discarded', reason: `installation is ${integration.status}` };
38
+ if (!resolved?.active) return { status: 'discarded', reason: resolved ? `trigger is ${resolved.inactive}` : 'trigger is not declared' };
39
+
40
+ if (resolved.overlap === 'allow') return this.run({ invoke, payload, count });
41
+
42
+ const type = `TRIGGER:${payload.trigger}`;
43
+ const [existing] = await this.processRepository.findByIntegrationAndType(integration.id, type);
44
+ const leaseMs = 2 * (resolved.tickMinutes || AT_LEASE_MINUTES) * 60000;
45
+ const touched = existing ? new Date(existing.updatedAt || existing.createdAt || 0).getTime() : 0;
46
+ if (existing && !TERMINAL.includes(existing.state) && this.now() - touched < leaseMs) {
47
+ count('trigger.skipped');
48
+ return { status: 'skipped', reason: `a run of ${payload.trigger} is still going` };
49
+ }
50
+
51
+ const context = { scheduledFor: payload.scheduledFor, startedAt: new Date(this.now()).toISOString() };
52
+ const record = existing
53
+ ? await this.processRepository.update(existing.id, { state: 'RUNNING', context })
54
+ : await this.processRepository.create({ userId: integration.userId, integrationId: integration.id, name: type, type, state: 'RUNNING', context, results: {} });
55
+ try {
56
+ const out = await this.run({ invoke, payload, count });
57
+ await this.processRepository.update(record.id, { state: 'COMPLETED', results: { scheduledFor: payload.scheduledFor, finishedAt: new Date(this.now()).toISOString() } });
58
+ return out;
59
+ } catch (error) {
60
+ await this.processRepository.update(record.id, { state: 'ERROR', results: { scheduledFor: payload.scheduledFor, error: error.message } }).catch(() => {});
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ async run({ invoke, payload, count }) {
66
+ try {
67
+ const result = await invoke(payload);
68
+ count('trigger.run');
69
+ return { status: 'ran', result };
70
+ } catch (error) {
71
+ count('trigger.failed');
72
+ throw error;
73
+ }
74
+ }
75
+ }
76
+
77
+ module.exports = { RunTrigger, TERMINAL };
@@ -19,48 +19,19 @@ function mapIntegrationClassToIntegrationDTO(integration) {
19
19
  };
20
20
  }
21
21
 
22
- let deprecationLogged = false;
23
22
 
24
- /**
25
- * @deprecated Since ADR-059. Read `entityTypeRegistry` / `moduleDefinitions`
26
- * from `loadAppDefinition()`, or call `getEntityTypeRegistry(appDefinition)`
27
- * and read each entry's `definition`.
28
- *
29
- * Kept as a wrapper over the entity type registry so callers that imported it
30
- * keep working. It returns the registry's definitions, so it follows the
31
- * registry's rules: one definition per entity type name (the first wins when
32
- * two integrations bind different definitions under one name, as
33
- * ModuleFactory already picked), and, when `entities` is passed, registry-only
34
- * types are included and conflicts throw `EntityRegistryError`.
35
- *
36
- * @param {Array<Function>} integrationClasses - integration classes
37
- * @param {Object} [entities] - `appDefinition.entities` (ADR-059)
38
- * @returns {Array<Object>} API module definitions
39
- */
40
- function getModulesDefinitionFromIntegrationClasses(
41
- integrationClasses,
42
- entities
43
- ) {
44
- // Required here, not at the top: modules/ must not load at require time
45
- // of every integration use case that only wants the DTO mapper.
46
- const {
47
- getEntityTypeRegistry,
48
- getEntityTypeDefinitions,
49
- } = require('../../modules/entity-type-registry');
50
- if (!deprecationLogged) {
51
- deprecationLogged = true;
52
- const { getLogger } = require('../../logs');
53
- getLogger('frigg.integrations').warn(
54
- 'getModulesDefinitionFromIntegrationClasses is deprecated; use getEntityTypeRegistry (ADR-059)',
55
- { eventName: 'frigg.integrations.deprecated_module_definitions' }
56
- );
57
- }
58
- return getEntityTypeDefinitions(
59
- getEntityTypeRegistry({ integrations: integrationClasses, entities })
60
- );
61
- }
62
-
63
- module.exports = {
64
- mapIntegrationClassToIntegrationDTO,
65
- getModulesDefinitionFromIntegrationClasses,
23
+ const getModulesDefinitionFromIntegrationClasses = (integrationClasses) => {
24
+ return [
25
+ ...new Set(
26
+ integrationClasses
27
+ .map((integration) =>
28
+ Object.values(integration.Definition.modules).map(
29
+ (module) => module.definition
30
+ )
31
+ )
32
+ .flat()
33
+ ),
34
+ ];
66
35
  };
36
+
37
+ module.exports = { mapIntegrationClassToIntegrationDTO, getModulesDefinitionFromIntegrationClasses };
package/modules/index.js CHANGED
@@ -4,15 +4,6 @@ const { OAuth2Requester } = require('./requester/oauth-2');
4
4
  const { Requester } = require('./requester/requester');
5
5
  const { ModuleConstants } = require('./ModuleConstants');
6
6
  const { ModuleFactory } = require('./module-factory');
7
- const {
8
- getEntityTypeRegistry,
9
- EntityRegistryError,
10
- } = require('./entity-type-registry');
11
- const {
12
- resolveScope,
13
- listBindingScopes,
14
- OwnershipConfigError,
15
- } = require('./entity-scope');
16
7
 
17
8
  module.exports = {
18
9
  ApiKeyRequester,
@@ -21,9 +12,4 @@ module.exports = {
21
12
  Requester,
22
13
  ModuleConstants,
23
14
  ModuleFactory,
24
- getEntityTypeRegistry,
25
- EntityRegistryError,
26
- resolveScope,
27
- listBindingScopes,
28
- OwnershipConfigError,
29
15
  };
@@ -45,18 +45,6 @@ class ModuleFactory {
45
45
  );
46
46
  }
47
47
 
48
- // definition.env may be an async function (e.g. the db-credentials
49
- // extension's bootstrap loads OAuth app credentials from the
50
- // database); resolve it first. A static env keeps the plain
51
- // constructor, exactly as before.
52
- if (typeof moduleDefinition.env === 'function') {
53
- return await Module.create({
54
- userId,
55
- entity,
56
- definition: moduleDefinition,
57
- });
58
- }
59
-
60
48
  return new Module({
61
49
  userId,
62
50
  entity,
package/modules/module.js CHANGED
@@ -21,17 +21,8 @@ class Module extends Delegate {
21
21
  * @param {string} params.userId The user id
22
22
  * @param {Object} params.entity The entity record from the database
23
23
  * @param {string} [params.state] Optional OAuth state value forwarded to the API client (round-trips through the OAuth provider).
24
- * @param {Object} [params.resolvedEnv] definition.env as Module.create() resolved it (may be undefined).
25
- * @param {boolean} [params.envResolved=false] true when resolvedEnv carries the resolved env, even an undefined one; otherwise the static definition.env is used.
26
24
  */
27
- constructor({
28
- definition,
29
- userId = null,
30
- entity: entityObj = null,
31
- state = null,
32
- resolvedEnv,
33
- envResolved = false,
34
- }) {
25
+ constructor({ definition, userId = null, entity: entityObj = null, state = null }) {
35
26
  super({ definition, userId, entity: entityObj });
36
27
 
37
28
  this.validateDefinition(definition);
@@ -63,23 +54,8 @@ class Module extends Delegate {
63
54
 
64
55
  Object.assign(this, this.definition.requiredAuthMethods);
65
56
 
66
- // A pre-resolved env (Module.create()) supports definition.env as an
67
- // async function, e.g. OAuth app credentials loaded from the database
68
- // by the db-credentials extension; otherwise the static object.
69
- // "Resolved" is an explicit flag, not a non-null value: a function env
70
- // may legitimately resolve to undefined (no static env, no DB row),
71
- // and falling back to definition.env would then read the function.
72
- const envValues = envResolved ? resolvedEnv : this.definition.env;
73
- if (typeof envValues === 'function') {
74
- // Spreading a function yields no keys, so the API client would
75
- // silently get no client_id / client_secret.
76
- throw new TypeError(
77
- `Module ${this.name}: definition.env is a function; build the module with await Module.create(...)`
78
- );
79
- }
80
-
81
57
  const apiParams = {
82
- ...envValues,
58
+ ...this.definition.env,
83
59
  delegate: this,
84
60
  logger: this.logger,
85
61
  ...(state ? { state } : {}),
@@ -91,33 +67,6 @@ class Module extends Delegate {
91
67
  this.api = new this.apiClass(apiParams);
92
68
  }
93
69
 
94
- /**
95
- * Resolves definition.env, supporting both a static object and a
96
- * (possibly async) function.
97
- *
98
- * @param {Object} definition - Module definition
99
- * @returns {Promise<Object|undefined>} Resolved env values
100
- */
101
- static async resolveEnv(definition) {
102
- if (typeof definition.env === 'function') {
103
- return await definition.env();
104
- }
105
- return definition.env;
106
- }
107
-
108
- /**
109
- * Async factory that resolves definition.env first, so a module whose
110
- * env is a function gets its values. Prefer it over `new Module(...)`
111
- * wherever the call site can await.
112
- *
113
- * @param {Object} params - same as the constructor, minus resolvedEnv
114
- * @returns {Promise<Module>}
115
- */
116
- static async create({ definition, userId = null, entity = null, state = null }) {
117
- const resolvedEnv = await Module.resolveEnv(definition);
118
- return new Module({ definition, userId, entity, state, resolvedEnv, envResolved: true });
119
- }
120
-
121
70
  getName() {
122
71
  return this.name;
123
72
  }
@@ -3,14 +3,12 @@ const {
3
3
  toObjectId,
4
4
  fromObjectId,
5
5
  findMany,
6
- findManyDrained,
7
6
  findOne,
8
7
  insertOne,
9
8
  updateOne,
10
9
  deleteOne,
11
10
  } = require('../../database/documentdb-utils');
12
11
  const { ModuleRepositoryInterface } = require('./module-repository-interface');
13
- const { entityNotFoundError } = require('./entity-not-found');
14
12
  const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
15
13
 
16
14
  /**
@@ -35,11 +33,11 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
35
33
  async findEntityById(entityId) {
36
34
  const objectId = toObjectId(entityId);
37
35
  if (!objectId) {
38
- throw entityNotFoundError(entityId);
36
+ throw new Error(`Entity ${entityId} not found`);
39
37
  }
40
38
  const doc = await findOne(this.prisma, 'Entity', { _id: objectId });
41
39
  if (!doc) {
42
- throw entityNotFoundError(entityId);
40
+ throw new Error(`Entity ${entityId} not found`);
43
41
  }
44
42
  const credential = await this._fetchCredential(doc.credentialId);
45
43
  return this._mapEntity(doc, credential);
@@ -78,29 +76,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
78
76
  return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
79
77
  }
80
78
 
81
- /**
82
- * Find entities matching any of the owner/organization pairs, in one query.
83
- * `organizationId: null` matches rows with no organization (a raw MongoDB
84
- * `null` also matches a missing field). The caller (a use case) decides
85
- * which pairs; this only runs them.
86
- *
87
- * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
88
- * @returns {Promise<Array>}
89
- */
90
- async findEntitiesByOwnership(clauses) {
91
- if (!Array.isArray(clauses) || clauses.length === 0) return [];
92
- // Drained: a list can hold more than the first cursor batch (~101).
93
- const docs = await findManyDrained(this.prisma, 'Entity', {
94
- $or: clauses.map(({ userId, organizationId }) => ({
95
- userId: toObjectId(userId),
96
- organizationId:
97
- organizationId === null ? null : toObjectId(organizationId),
98
- })),
99
- });
100
- const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
101
- return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
102
- }
103
-
104
79
  async unsetCredential(entityId) {
105
80
  const objectId = toObjectId(entityId);
106
81
  if (!objectId) return false;
@@ -140,14 +115,7 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
140
115
  );
141
116
  }
142
117
 
143
- /**
144
- * @param {Object} entityData
145
- * @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
146
- * belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
147
- * because `entityData` carries provider fields and one named `organizationId`
148
- * must not be taken for ours.
149
- */
150
- async createEntity(entityData, tenancy = {}) {
118
+ async createEntity(entityData) {
151
119
  const {
152
120
  user,
153
121
  userId,
@@ -165,7 +133,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
165
133
  name: name ?? null,
166
134
  moduleName: moduleName ?? null,
167
135
  externalId: externalId ?? null,
168
- organizationId: toObjectId(tenancy.organizationId) || null,
169
136
  data: dynamicData,
170
137
  };
171
138
  const insertedId = await insertOne(this.prisma, 'Entity', document);
@@ -280,9 +247,8 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
280
247
  const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
281
248
  if (objectIds.length === 0) return new Map();
282
249
 
283
- // Raw read to bypass the Prisma encryption extension. Drained:
284
- // more ids than the first cursor batch must all come back.
285
- const rawCredentials = await findManyDrained(this.prisma, 'Credential', {
250
+ // Use raw findMany to bypass Prisma encryption extension
251
+ const rawCredentials = await findMany(this.prisma, 'Credential', {
286
252
  _id: { $in: objectIds }
287
253
  });
288
254
 
@@ -363,30 +329,19 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
363
329
  if (filter.name) query.name = filter.name;
364
330
  if (filter.moduleName) query.moduleName = filter.moduleName;
365
331
  if (filter.externalId) query.externalId = filter.externalId;
366
- // `null` means "no organization"; absent means "any". A raw MongoDB
367
- // `{ field: null }` matches both a null value and a missing field.
368
- if (filter.organizationId !== undefined) {
369
- query.organizationId =
370
- filter.organizationId === null
371
- ? null
372
- : toObjectId(filter.organizationId);
373
- }
374
332
  return query;
375
333
  }
376
334
 
377
335
  _mapEntity(doc, credential) {
378
336
  const dynamicData = doc?.data || {};
379
337
  return {
380
- // First, so no stored provider field can replace a column below
381
- // (identity, owner or organization decide who sees the entity).
382
- ...dynamicData,
383
338
  id: fromObjectId(doc?._id),
384
339
  credential,
385
340
  userId: fromObjectId(doc?.userId),
386
341
  name: doc?.name ?? null,
387
342
  externalId: doc?.externalId ?? null,
388
343
  moduleName: doc?.moduleName ?? null,
389
- organizationId: fromObjectId(doc?.organizationId) ?? null,
344
+ ...dynamicData,
390
345
  };
391
346
  }
392
347
  }
@@ -67,21 +67,6 @@ class ModuleRepositoryInterface {
67
67
  );
68
68
  }
69
69
 
70
- /**
71
- * Find entities matching any of the owner/organization pairs, in one query
72
- * (ADR-056 §3.6). `organizationId: null` matches rows with no organization.
73
- * Which pairs is decided by the caller (modules/entity-scope.js), not here.
74
- *
75
- * @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
76
- * @returns {Promise<Array>} Array of entity objects
77
- * @abstract
78
- */
79
- async findEntitiesByOwnership(clauses) {
80
- throw new Error(
81
- 'Method findEntitiesByOwnership must be implemented by subclass'
82
- );
83
- }
84
-
85
70
  /**
86
71
  * Unset credential from entity
87
72
  *
@@ -125,13 +110,11 @@ class ModuleRepositoryInterface {
125
110
  /**
126
111
  * Create a new entity
127
112
  *
128
- * @param {Object} entityData - Entity data (provider fields included)
129
- * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization
130
- * the entity belongs to (ADR-056 §3.6), kept apart from `entityData`
113
+ * @param {Object} entityData - Entity data
131
114
  * @returns {Promise<Object>} Created entity object
132
115
  * @abstract
133
116
  */
134
- async createEntity(entityData, tenancy) {
117
+ async createEntity(entityData) {
135
118
  throw new Error('Method createEntity must be implemented by subclass');
136
119
  }
137
120