@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,137 @@
1
+ /**
2
+ * The configure form for a data-defined installation, generated from its definition: what GET_CONFIG_OPTIONS and
3
+ * REFRESH_CONFIG_OPTIONS return on Frigg's existing, authenticated /api/integrations/:id/config/options routes.
4
+ *
5
+ * The form's data has exactly the shape of Integration.config, so what the management UI PATCHes back is the config:
6
+ * { settings: { … }, syncs: { <id>: { enabled } }, workflows: { <id>: { enabled, settings: { … } } }, ui: { <id>: { enabled } } }
7
+ *
8
+ * Option settings become enums filled by `options(path, values)` (resolveOptions underneath). A list whose inputs are
9
+ * not chosen yet comes back empty with a help line saying what to choose first; REFRESH_CONFIG_OPTIONS, called with
10
+ * the form's current values, fills it.
11
+ */
12
+ const SETTING_SCHEMA = {
13
+ string: () => ({ type: 'string' }),
14
+ text: () => ({ type: 'string' }),
15
+ number: () => ({ type: 'number' }),
16
+ boolean: () => ({ type: 'boolean' }),
17
+ duration: () => ({ type: 'string', pattern: '^\\d+(m|h|d)$' }),
18
+ enum: (s) => ({ type: 'string', enum: s.enum }),
19
+ };
20
+
21
+ const dependsOn = (s) => (s && typeof s.options === 'object' ? Object.values(s.options.with || {}).map((v) => String(v).replace(/^\$\.(settings|install)\./, '')) : []);
22
+
23
+ async function settingsSchema(settings, pathPrefix, values, options, integrationSettings) {
24
+ const properties = {}, ui = {}, required = [];
25
+ for (const [key, s] of Object.entries(settings || {})) {
26
+ let schema;
27
+ if (s.type === 'option') {
28
+ const choices = await options(`${pathPrefix}.${key}`, values, integrationSettings);
29
+ schema = choices.length
30
+ ? { type: 'string', oneOf: choices.map((c) => ({ const: String(c.value), title: c.label })) }
31
+ : { type: 'string', enum: [] };
32
+ if (!choices.length && dependsOn(s).length) ui[key] = { 'ui:help': `Choose ${dependsOn(s).join(' and ')} first.` };
33
+ } else schema = (SETTING_SCHEMA[s.type] || SETTING_SCHEMA.string)(s);
34
+ properties[key] = { ...schema, title: s.label, ...(s.default !== undefined ? { default: s.default } : {}) };
35
+ if (s.help) ui[key] = { ...(ui[key] || {}), 'ui:help': s.help };
36
+ if (s.required && s.default === undefined) required.push(key);
37
+ }
38
+ return { schema: { type: 'object', properties, ...(required.length ? { required } : {}) }, ui };
39
+ }
40
+
41
+ /**
42
+ * The timing field of a schedule the definition marks configurable: an interval when its default is an interval,
43
+ * a cron (with time zone) when its default is a cron. The floor is said in the help text and enforced on save.
44
+ */
45
+ function scheduleField(recurrence, configurable) {
46
+ if (!configurable || !recurrence) return null;
47
+ const min = configurable.min ? ` No more often than every ${configurable.min}.` : '';
48
+ if (recurrence.every !== undefined) {
49
+ return { schema: { type: 'object', title: 'Schedule', properties: { every: { type: 'string', title: 'Runs every', pattern: '^\\d+\\s*(m|h|d)$', default: recurrence.every } } }, ui: { every: { 'ui:help': `Minutes, hours or days: 15m, 2h, 1d.${min}` } } };
50
+ }
51
+ return { schema: { type: 'object', title: 'Schedule', properties: { cron: { type: 'string', title: 'Cron', default: recurrence.cron }, timezone: { type: 'string', title: 'Time zone', default: recurrence.timezone || 'UTC' } } }, ui: { cron: { 'ui:help': `minute hour day-of-month month day-of-week.${min}` } } };
52
+ }
53
+ const configurableOf = (c) => (c ? (c === true ? {} : c) : false);
54
+
55
+ const toggle = (title, description, enabledByDefault) => ({ type: 'boolean', title, ...(description ? { description } : {}), default: enabledByDefault });
56
+ const isOn = (item, chosen) => (typeof chosen?.enabled === 'boolean' ? chosen.enabled : (item.default || 'on') === 'on');
57
+
58
+ /**
59
+ * @param {object} def
60
+ * @param {object} config the installation's current config (or the form's current values on refresh)
61
+ * @param {(path: string, values: object, integrationSettings: object) => Promise<Array<{label,value}>>} options
62
+ * @returns {Promise<{ jsonSchema: object, uiSchema: object, data: object }>}
63
+ */
64
+ async function buildConfigForm(def, config = {}, options = async () => []) {
65
+ const jsonSchema = { type: 'object', title: def.display?.label, properties: {} };
66
+ const uiSchema = {};
67
+ const data = { settings: { ...(config.settings || {}) }, syncs: {}, workflows: {}, ui: {} };
68
+
69
+ if (Object.keys(def.settings || {}).length) {
70
+ const s = await settingsSchema(def.settings, 'settings', config.settings || {}, options, config.settings || {});
71
+ jsonSchema.properties.settings = { title: 'Settings', ...s.schema };
72
+ uiSchema.settings = s.ui;
73
+ }
74
+
75
+ if ((def.syncs || []).length) {
76
+ jsonSchema.properties.syncs = { type: 'object', title: 'Syncs', properties: {} };
77
+ for (const s of def.syncs) {
78
+ jsonSchema.properties.syncs.properties[s.id] = { type: 'object', title: s.label, properties: { enabled: toggle('On', s.description, (s.default || 'on') === 'on') } };
79
+ data.syncs[s.id] = { enabled: isOn(s, config.syncs?.[s.id]) };
80
+ const field = s.ongoing?.mode === 'poll' && scheduleField({ every: s.ongoing.every || '15m' }, configurableOf(s.ongoing.configurable));
81
+ if (field) {
82
+ jsonSchema.properties.syncs.properties[s.id].properties.schedule = field.schema;
83
+ ((uiSchema.syncs ||= {})[s.id] ||= {}).schedule = field.ui;
84
+ if (config.syncs?.[s.id]?.schedule) data.syncs[s.id].schedule = { ...config.syncs[s.id].schedule };
85
+ }
86
+ }
87
+ }
88
+
89
+ if ((def.workflows || []).length) {
90
+ jsonSchema.properties.workflows = { type: 'object', title: 'Workflows', properties: {} };
91
+ uiSchema.workflows = {};
92
+ for (const w of def.workflows) {
93
+ const chosen = config.workflows?.[w.id] || {};
94
+ const node = { type: 'object', title: w.label, properties: { enabled: toggle('On', w.description, (w.default || 'on') === 'on') } };
95
+ data.workflows[w.id] = { enabled: isOn(w, chosen), settings: { ...(chosen.settings || {}) } };
96
+ if (Object.keys(w.settings || {}).length) {
97
+ // A workflow setting's option inputs may read integration settings, which it sees as $.install.
98
+ const s = await settingsSchema(w.settings, `workflows.${w.id}.settings`, chosen.settings || {}, options, config.settings || {});
99
+ node.properties.settings = { title: 'Settings', ...s.schema };
100
+ uiSchema.workflows[w.id] = { settings: s.ui };
101
+ // Settings only matter while the workflow is on: required only then.
102
+ node.dependencies = { enabled: { oneOf: [{ properties: { enabled: { const: false } } }, { properties: { enabled: { const: true } }, required: ['settings'] }] } };
103
+ }
104
+ const t = w.trigger || {};
105
+ const field = t.type === 'schedule' && scheduleField(t.every !== undefined ? { every: t.every } : { cron: t.cron, timezone: t.timezone }, configurableOf(t.configurable));
106
+ if (field) {
107
+ node.properties.schedule = field.schema;
108
+ (uiSchema.workflows[w.id] ||= {}).schedule = field.ui;
109
+ if (chosen.schedule) data.workflows[w.id].schedule = { ...chosen.schedule };
110
+ }
111
+ jsonSchema.properties.workflows.properties[w.id] = node;
112
+ }
113
+ }
114
+
115
+ if ((def.ui || []).length) {
116
+ jsonSchema.properties.ui = { type: 'object', title: 'Cards and panels', properties: {} };
117
+ for (const u of def.ui) {
118
+ jsonSchema.properties.ui.properties[u.id] = { type: 'object', title: u.label || u.id, properties: { enabled: toggle('Show', `${u.host} · ${u.placement}`, (u.default || 'on') === 'on') } };
119
+ data.ui[u.id] = { enabled: isOn(u, config.ui?.[u.id]) };
120
+ }
121
+ }
122
+
123
+ return { jsonSchema, uiSchema, data };
124
+ }
125
+
126
+ /** Is anything required still missing? (NEEDS_CONFIG) */
127
+ function missingRequired(def, config = {}) {
128
+ const missing = [];
129
+ for (const [k, s] of Object.entries(def.settings || {})) if (s.required && s.default === undefined && (config.settings?.[k] == null || config.settings[k] === '')) missing.push(`settings.${k}`);
130
+ for (const w of def.workflows || []) {
131
+ if (!isOn(w, config.workflows?.[w.id])) continue;
132
+ for (const [k, s] of Object.entries(w.settings || {})) if (s.required && s.default === undefined && (config.workflows?.[w.id]?.settings?.[k] == null || config.workflows[w.id].settings[k] === '')) missing.push(`workflows.${w.id}.settings.${k}`);
133
+ }
134
+ return missing;
135
+ }
136
+
137
+ module.exports = { buildConfigForm, missingRequired };
@@ -0,0 +1,417 @@
1
+ /**
2
+ * DataDefinedIntegration: one IntegrationBase subclass that runs any published definition.
3
+ *
4
+ * Frigg registers integration classes at startup (appDefinition.integrations). For data-defined integrations the app
5
+ * registers one generated class per definition id:
6
+ *
7
+ * const { defineIntegrationClass } = require('@friggframework/core/integrations/data-defined');
8
+ * integrations: [ defineIntegrationClass({ id: 'phone-hubspot', definitions, manifests, modules: { phone: PhoneModule, hubspot: HubSpotModule } }) ]
9
+ *
10
+ * The class's static Definition (name, modules) is what the infrastructure needs, and it is the same for every
11
+ * version, so publishing a new version deploys nothing. Each installation loads the exact version it pins
12
+ * (record.config.definition.version) when it is hydrated, compiles it with that installation's choices, and gets an
13
+ * ordinary `this.events` map. Everything after that is the existing Frigg machinery: the event dispatcher, the
14
+ * queue worker, the scheduler and the mapping repository.
15
+ *
16
+ * Ports used: definitions.get(id, version) -> definition doc (immutable; cache freely)
17
+ * runCode(ref, args) -> registered code steps (optional)
18
+ */
19
+ const { compile, routeWebhook } = require('./compile');
20
+ const { makePorts, runWorkflow, runQuery, renderSurface, runLifecycle, resolveOptions } = require('./runtime');
21
+ const { eventsIn, handshakeFor, modulesFor } = require('./webhooks');
22
+ const { buildConfigForm, missingRequired } = require('./config-form');
23
+
24
+ /** The tick every data-defined trigger runs on: a chosen interval is rounded up to whole multiples of it. */
25
+ const INSTANCE_TICK = '5m';
26
+
27
+ /** A compiled definition's triggers as IntegrationBase trigger declarations: each runs its compiled event. */
28
+ function triggersOf(def, config) {
29
+ return compile(def, config).triggers.map((t) => ({
30
+ ...t,
31
+ tick: INSTANCE_TICK,
32
+ handler(payload) {
33
+ const event = this.events?.[t.name];
34
+ if (!event) throw new Error(`Trigger "${t.name}" has no compiled event in this installation`);
35
+ return event.handler({ data: { integrationId: this.id, ...payload } });
36
+ },
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * Shape A — one class per definition id (defineIntegrationClass): the class name and modules come from the
42
+ * definition. Publishing a new VERSION deploys nothing; a new definition ID is a new class (a deploy, and its own
43
+ * functions and queue). Right for a product team with a handful of integrations.
44
+ *
45
+ * Shape B — one class for every definition (defineSharedIntegrationClass): registered once as "data-defined", its
46
+ * static modules are every API module the app has installed, and each installation names its definition id and
47
+ * version in config. A new integration is a row, not a deploy, and every installation shares one set of functions
48
+ * and one queue. Right for a multi-tenant product, where definitions accumulate without bound.
49
+ */
50
+ function defineIntegrationClass({ id, definitions, manifests, modules, runCode, IntegrationBase, lookup, runs }) {
51
+ const latest = definitions.latest(id);
52
+ if (!latest || typeof latest.then === 'function') throw new Error(`defineIntegrationClass needs the definitions of "${id}" synchronously at registration (bundled files); a database store goes with defineSharedIntegrationClass`);
53
+ return makeClass({
54
+ name: latest.id, defaultId: id, definitions, manifests, runCode, IntegrationBase, lookup, runs,
55
+ modules: Object.fromEntries(Object.keys(latest.modules).map((key) => [key, { definition: modules[key] }])),
56
+ display: { label: latest.display.label, description: latest.display.description, category: latest.display.category },
57
+ version: latest.version,
58
+ });
59
+ }
60
+
61
+ function defineSharedIntegrationClass({ name = 'data-defined', definitions, manifests, modules, runCode, IntegrationBase, lookup, runs }) {
62
+ return makeClass({
63
+ name, defaultId: null, definitions, manifests, runCode, IntegrationBase, lookup, runs,
64
+ modules: Object.fromEntries(Object.entries(modules).map(([key, definition]) => [key, { definition }])),
65
+ display: { label: 'Data-defined integration', description: 'Runs any published integration definition.', category: 'Custom' },
66
+ version: '0.1.0',
67
+ });
68
+ }
69
+
70
+ /**
71
+ * App-level webhooks (one URL for every account: HubSpot, Stripe Connect, GitHub Apps) arrive with no integration id.
72
+ * The default lookup finds the installations of THIS class connected to that account, using Frigg's commands: the
73
+ * entity reverse lookup, then each candidate's record, because one account can be used by integrations of other
74
+ * types too, and the worker would hydrate those as their own class.
75
+ */
76
+ function friggLookup(typeName) {
77
+ return async (externalId, moduleName) => {
78
+ const { createIntegrationCommands } = require('../../application/commands/integration-commands');
79
+ const commands = createIntegrationCommands();
80
+ const ids = await commands.listIntegrationsByEntityExternalId(externalId, moduleName);
81
+ const out = [];
82
+ for (const integrationId of ids) {
83
+ const found = await commands.loadIntegrationContextById(integrationId);
84
+ if (found?.context?.record?.config?.type === typeName) out.push(integrationId);
85
+ }
86
+ return out;
87
+ };
88
+ }
89
+
90
+ function makeClass({ name, defaultId, definitions, manifests, runCode, IntegrationBase, modules, display, version, lookup, runs }) {
91
+ const Base = IntegrationBase || require('../integration-base').IntegrationBase;
92
+ const id = name;
93
+ const findInstallations = lookup || friggLookup(name);
94
+ // Where workflow runs are kept: the WorkflowRun table by default (loaded lazily, so tests and tools without a
95
+ // database can pass their own store or `false` to keep none).
96
+ let runStore = runs;
97
+ const runRepository = () => (runStore === undefined ? (runStore = require('./repositories/workflow-run-repository').createWorkflowRunRepository()) : runStore);
98
+ const publicBase = () => `${process.env.FRIGG_PUBLIC_URL || ''}/api/${id}-integration/webhooks`;
99
+ const pinnedDefinitions = new Map(); // `${id}@${version}` -> definition, for the trigger tick
100
+
101
+ class DataDefinedIntegration extends Base {
102
+ // version is display only; installations pin their own. instanceTriggers: schedule-triggered workflows and
103
+ // polling syncs are this class's recurring triggers, resolved per installation (triggersFor) on one 5m tick.
104
+ static Definition = { name, version, modules, webhooks: { enabled: true }, display, instanceTriggers: { tick: INSTANCE_TICK } };
105
+
106
+ /**
107
+ * The recurring triggers of one installation (ADR-055): its pinned definition compiled with its config, so a
108
+ * switched-off workflow has none and a configurable one carries the installation's choice. Each runs its
109
+ * compiled event. Read by the shared tick from the record alone (no modules, no credentials).
110
+ */
111
+ static async triggersFor({ config = {} } = {}) {
112
+ const pin = config.definition || (defaultId ? { id: defaultId } : null);
113
+ if (!pin?.id) return [];
114
+ let def;
115
+ if (pin.version) {
116
+ // Published versions are immutable, so a tick over many installations reads each one once.
117
+ const key = `${pin.id}@${pin.version}`;
118
+ if (!pinnedDefinitions.has(key)) {
119
+ if (pinnedDefinitions.size >= 100) pinnedDefinitions.clear();
120
+ pinnedDefinitions.set(key, await definitions.get(pin.id, pin.version, { checksum: pin.checksum }));
121
+ }
122
+ def = pinnedDefinitions.get(key);
123
+ } else {
124
+ def = await definitions.latest(pin.id);
125
+ }
126
+ return def ? triggersOf(def, config) : [];
127
+ }
128
+
129
+ /** How the routers find data-defined classes and the store and manifests they run on. */
130
+ static DataDefined = { definitions, manifests, shared: !defaultId, runs: () => runRepository() };
131
+
132
+ /**
133
+ * Frigg's use cases construct an integration from its record and call initialize() before anything else
134
+ * (create, update, delete, webhooks, user actions, config options). So this is where the pinned definition is
135
+ * loaded: every real entry point hydrates through here. A dry instance (no record, e.g. an app-level webhook
136
+ * before its installation is known) has no pin and loads nothing.
137
+ */
138
+ async initialize() {
139
+ if (this.config?.definition || defaultId) await this.loadDefinition();
140
+ return super.initialize();
141
+ }
142
+
143
+ async loadDefinition() {
144
+ let pin = this.config?.definition || (defaultId ? { id: defaultId } : null);
145
+ if (!pin?.id) throw new Error('This installation does not name a definition (config.definition.id)');
146
+ if (!pin.version) {
147
+ // Installed by id alone: pin the latest published version now (onCreate persists the pin).
148
+ const latest = await definitions.latest(pin.id);
149
+ if (!latest) throw new Error(`No published version of "${pin.id}"`);
150
+ pin = { id: pin.id, version: latest.version, checksum: await definitions.checksumOf?.(pin.id, latest.version) || undefined };
151
+ if (this.config?.definition) this.config = { ...this.config, definition: pin };
152
+ }
153
+ this.def = await definitions.get(pin.id, pin.version, { checksum: pin.checksum });
154
+ if (!this.def) throw new Error(`Definition ${pin.id}@${pin.version} is not published`);
155
+ this.compiled = compile(this.def, this.config || {});
156
+ this.ports = this.portsFor(this.def);
157
+ // Replace, not merge: a workflow switched off since the last compile must lose its event.
158
+ for (const n of this.compiledEventNames || []) delete this.events[n];
159
+ this.compiledEventNames = Object.keys(this.compiled.events);
160
+ for (const [n, e] of Object.entries(this.compiled.events)) {
161
+ this.events[n] = { ...e, handler: (payload) => this.dispatch(e.ref, payload) };
162
+ }
163
+ this.registerEventHandlers();
164
+ return this.def;
165
+ }
166
+
167
+ /** Ports for a definition: Api calls through the manifest, and links/dedupe over IntegrationMapping. */
168
+ portsFor(def) {
169
+ return makePorts({
170
+ def, manifests, runCode,
171
+ // The shared class registers modules by module name; a definition may name them by its own keys.
172
+ callApi: (moduleKey, method, args) => this.moduleFor(moduleKey).api[method](args),
173
+ store: {
174
+ get: async (key) => (await this.getMapping(key))?.mapping || null,
175
+ put: (key, value) => this.upsertMapping(key, value),
176
+ claim: async (key, value) => { try { await this.createMapping(key, value); return true; } catch (e) { if (e.name === 'MappingAlreadyExistsError') return false; throw e; } },
177
+ release: (key) => this.deleteMapping(key),
178
+ },
179
+ });
180
+ }
181
+
182
+ moduleFor(key) {
183
+ const m = this[key] || this[this.def?.modules?.[key]?.module];
184
+ if (!m?.api) throw new Error(`Module "${key}" is not connected to this installation`);
185
+ return m;
186
+ }
187
+
188
+ async dispatch(ref, payload = {}) {
189
+ const msg = payload.data || payload;
190
+ // A message queued under an older version runs on that version (an upgrade drains, it never re-interprets).
191
+ const def = msg.definitionVersion && msg.definitionVersion !== this.def.version
192
+ ? await definitions.get(this.def.id, msg.definitionVersion) || this.def
193
+ : this.def;
194
+ const installation = this.installation();
195
+ // A trigger run may be delivered twice (a redelivery, two ticks in one slot); the trigger and the time it
196
+ // was scheduled for make its dedupe id.
197
+ const eventId = msg.eventId || (msg.trigger && msg.scheduledFor ? `tick:${msg.trigger}:${msg.scheduledFor}` : undefined);
198
+ if (ref.kind === 'workflow') return this.recordRun(await runWorkflow({ def, installation, workflowId: ref.id, event: msg.body ?? msg, eventId, record: msg.record, ports: this.ports, manifests }));
199
+ if (ref.kind === 'query') return runQuery({ def, installation, queryId: ref.id, record: msg.record, ports: this.ports });
200
+ if (ref.kind === 'ui') return renderSurface({ def, installation, surfaceId: ref.id, record: payload.req?.body?.record || msg.record, ports: this.ports });
201
+ if (ref.kind === 'sync') return this.runSync(ref, msg);
202
+ throw new Error(`Unknown event ref ${JSON.stringify(ref)}`);
203
+ }
204
+
205
+ /**
206
+ * Receipt (no database): answer a provider handshake synchronously (Slack url_verification, Asana
207
+ * X-Hook-Secret), otherwise queue the delivery as Frigg always does. The URL names the module (?module=).
208
+ */
209
+ async onWebhookReceived({ req, res }) {
210
+ const manifest = manifests[req.query?.module];
211
+ const h = manifest && handshakeFor(manifest, { body: req.body, headers: req.headers, query: req.query });
212
+ if (h) {
213
+ for (const [k, v] of Object.entries(h.headers || {})) res.set(k, String(v));
214
+ return typeof h.body === 'object' ? res.status(h.status).json(h.body) : res.status(h.status).send(h.body);
215
+ }
216
+ return super.onWebhookReceived({ req, res });
217
+ }
218
+
219
+ /**
220
+ * Worker side of a raw delivery. Without an installation (an app-level webhook), find the installations
221
+ * connected to each account the delivery is about and queue it for each. With one: verify it, split it into
222
+ * module events, and queue one message per enabled workflow listening for each event.
223
+ */
224
+ async onWebhook({ data = {} }) {
225
+ if (data.module && data.topic) return this.routeEvents(data.module, [{ topic: data.topic, event: data.body, eventId: data.eventId || data.body?.id || null }]);
226
+ const delivery = { body: data.body, headers: data.headers || {}, query: data.query || {} };
227
+ if (!this.def) return this.fanOutAppWebhook(delivery);
228
+
229
+ const keys = modulesFor(this.def, manifests, delivery);
230
+ if (keys.length !== 1) { this.logWebhook('webhook.unrouted', { module: delivery.query.module || null, candidates: keys }); return { routed: 0 }; }
231
+ const key = keys[0];
232
+ const manifest = manifests[this.def.modules[key].module];
233
+ if (manifest.webhook?.verify) {
234
+ let ok = false;
235
+ try { ok = await this.moduleFor(key).api[manifest.webhook.verify]({ headers: delivery.headers, body: delivery.body, query: delivery.query }); } catch { ok = false; }
236
+ if (ok !== true) { this.logWebhook('webhook.rejected', { module: key }); return { routed: 0, rejected: true }; }
237
+ }
238
+ const own = this.accountOf(key);
239
+ const events = eventsIn(manifest, delivery).filter((e) => manifest.webhook?.scope !== 'app' || !own || !e.account || String(e.account) === String(own));
240
+ return this.routeEvents(key, events);
241
+ }
242
+
243
+ async routeEvents(moduleKey, events) {
244
+ let routed = 0;
245
+ for (const e of events) {
246
+ for (const n of routeWebhook(this.compiled, moduleKey, e.topic)) {
247
+ await this.queueWebhook({ event: n, integrationId: this.id, module: moduleKey, topic: e.topic, body: e.event, eventId: e.eventId, definitionVersion: this.def.version });
248
+ routed += 1;
249
+ }
250
+ }
251
+ return { routed };
252
+ }
253
+
254
+ async fanOutAppWebhook(delivery) {
255
+ const moduleName = delivery.query.module;
256
+ const manifest = manifests[moduleName];
257
+ if (!manifest?.webhook?.account) { this.logWebhook('webhook.unrouted', { module: moduleName || null, reason: 'no installation and no account to look it up by' }); return { queued: 0 }; }
258
+ const accounts = new Set(eventsIn(manifest, delivery).map((e) => e.account).filter(Boolean));
259
+ let queued = 0;
260
+ for (const account of accounts) {
261
+ for (const integrationId of await findInstallations(account, moduleName)) {
262
+ await this.queueWebhook({ integrationId, body: delivery.body, headers: delivery.headers, query: delivery.query });
263
+ queued += 1;
264
+ }
265
+ }
266
+ return { queued };
267
+ }
268
+
269
+ /** The connected account's id for a module (its entity externalId): what app-level events are matched on. */
270
+ accountOf(key) {
271
+ const m = this[key] || this[this.def?.modules?.[key]?.module] || this.modules?.[this.def?.modules?.[key]?.module];
272
+ return m?.entity?.externalId ?? null;
273
+ }
274
+
275
+ logWebhook(eventName, fields) {
276
+ try { require('../../logs').getLogger('frigg.data-defined').info('Webhook not routed', { eventName: `frigg.data-defined.${eventName}`, integrationId: this.id || null, ...fields }); } catch { /* logging must never break delivery */ }
277
+ }
278
+
279
+ async onCreate(params) {
280
+ if (!this.def) await this.loadDefinition();
281
+ const out = await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onInstall', ports: this.ports });
282
+ // Every onInstall step with an id keeps its result, so onUninstall can undo exactly what install did
283
+ // ($.install.runtime.installed.<step id>). Saved even when empty, so a re-install replaces stale ids.
284
+ const installed = Object.fromEntries((this.def.lifecycle?.onInstall || []).filter((st) => st.id).map((st) => [st.id, out.steps?.[st.id] ?? null]));
285
+ await this.patchConfig({ ...(this.config?.definition ? { definition: this.config.definition } : {}), runtime: { ...(this.config?.runtime || {}), installed } });
286
+ return super.onCreate(params);
287
+ }
288
+
289
+ async onUpdate(params) {
290
+ await super.onUpdate(params); // merges config (enabled workflows, settings, mapping overrides)
291
+ return this.loadDefinition(); // recompile: enabling a workflow adds its event, disabling removes it
292
+ }
293
+
294
+ async onDelete(params) {
295
+ if (!this.def) await this.loadDefinition();
296
+ await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onUninstall', ports: this.ports });
297
+ return super.onDelete(params);
298
+ }
299
+
300
+ /** This installation's triggers, from the definition it already loaded and its current config. */
301
+ async triggerDeclarations() {
302
+ return this.def ? triggersOf(this.def, this.config || {}) : this.constructor.triggersFor({ config: this.config || {} });
303
+ }
304
+
305
+ /** The configure form (GET /api/integrations/:id/config/options), generated from the definition. */
306
+ async getConfigOptions() {
307
+ return buildConfigForm(this.def, this.config || {}, (path, values, integrationSettings) => this.optionsFor(path, values, integrationSettings));
308
+ }
309
+
310
+ /** The same form re-filled from the values chosen so far (POST …/config/options/refresh): dependent dropdowns. */
311
+ async refreshConfigOptions(params = {}) {
312
+ const values = params.config || params.formData || params.data || params;
313
+ return buildConfigForm(this.def, { ...(this.config || {}), ...values }, (path, v, integrationSettings) => this.optionsFor(path, v, integrationSettings));
314
+ }
315
+
316
+ async optionsFor(path, values, integrationSettings) {
317
+ try { return await resolveOptions({ def: this.def, manifests, ports: this.ports, path, values, integrationSettings }); } catch { return []; }
318
+ }
319
+
320
+ /** Required settings come from the definition; checked without calling any API (the base class renders the form). */
321
+ async validateConfig() {
322
+ return missingRequired(this.def, this.config || {}).length > 0;
323
+ }
324
+
325
+ /** The input form for a button workflow (POST …/actions/:id/options): the inputs of the UI buttons that run it. */
326
+ async getActionOptions(actionId) {
327
+ const ref = this.compiled?.events?.[actionId]?.ref;
328
+ const inputs = {};
329
+ const visit = (node) => { if (!node) return; const a = node.props?.action; if (a?.type === 'userAction' && a.workflow === ref?.id) Object.assign(inputs, a.input || {}); (node.children || []).forEach(visit); };
330
+ if (ref?.kind === 'workflow') (this.def.ui || []).forEach((u) => visit(u.view));
331
+ const properties = Object.fromEntries(Object.entries(inputs).map(([k, f]) => [k, { type: f.type === 'number' ? 'number' : 'string', title: f.label || k }]));
332
+ return { jsonSchema: Object.keys(properties).length ? { type: 'object', properties: { input: { type: 'object', properties } } } : {}, uiSchema: {} };
333
+ }
334
+
335
+ installation() {
336
+ const base = publicBase();
337
+ const webhookUrls = Object.fromEntries(Object.entries(this.def?.modules || {}).map(([key, m]) => [key, manifests[m.module]?.webhook?.scope === 'app'
338
+ ? `${base}?module=${encodeURIComponent(m.module)}`
339
+ : `${base}/${this.id}?module=${encodeURIComponent(m.module)}`]));
340
+ return { ...(this.config || {}), integrationId: this.id, accounts: this.accounts(), webhookUrl: `${base}/${this.id}`, webhookUrls };
341
+ }
342
+
343
+ /** $.install.accounts: per module key, the account fields its manifest declares, read from the module's Api
344
+ * (where apiPropertiesToPersist land after auth). An undeclared property is never exposed. */
345
+ accounts() {
346
+ return Object.fromEntries(Object.entries(this.def?.modules || {}).map(([key, m]) => {
347
+ const fields = manifests[m.module]?.account?.fields || [];
348
+ let api = {};
349
+ try { api = this.moduleFor(key).api || {}; } catch { api = {}; }
350
+ return [key, Object.fromEntries(fields.map((f) => [f.name, api[f.name] ?? null]))];
351
+ }));
352
+ }
353
+
354
+ /**
355
+ * Move this installation to another published version (default: the latest). With apply false, only the plan:
356
+ * what a person must supply first and what will happen. Applying refuses while anything is needed, and when
357
+ * install steps changed, undoes the old ones before running the new ones (it never leaves the old webhooks).
358
+ */
359
+ async upgradeTo(toVersion, { apply = false } = {}) {
360
+ const { PlanInstallationUpgrade } = require('./use-cases/definition-use-cases');
361
+ const plan = await new PlanInstallationUpgrade({ catalog: definitions }).execute({ config: this.config, toVersion });
362
+ if (!apply || !plan.allowed) return { ...plan, applied: false };
363
+ const reinstall = plan.actions.some((a) => a.kind === 'reinstall');
364
+ let config = plan.config;
365
+ if (reinstall) {
366
+ await runLifecycle({ def: this.def, installation: this.installation(), hook: 'onUninstall', ports: this.ports });
367
+ const next = await definitions.get(plan.config.definition.id, plan.config.definition.version);
368
+ const out = await runLifecycle({ def: next, installation: { ...this.installation(), ...config }, hook: 'onInstall', ports: this.portsFor(next) });
369
+ config = { ...config, runtime: { ...(this.config?.runtime || {}), installed: Object.fromEntries((next.lifecycle?.onInstall || []).filter((st) => st.id).map((st) => [st.id, out.steps?.[st.id] ?? null])) } };
370
+ }
371
+ await this.patchConfig(config);
372
+ this.config = { ...this.config, ...config };
373
+ await this.loadDefinition();
374
+ // Triggers need nothing here: the next tick reads the new pin and config.
375
+ return { ...plan, config, applied: true };
376
+ }
377
+
378
+ async recordRun(run) {
379
+ this.telemetry?.count?.('workflow.run', 1, { workflow: run.workflowId, status: run.status });
380
+ // Run history must never decide the run's outcome: a failed write is logged and the run stands.
381
+ try {
382
+ const store = runRepository();
383
+ if (store) await store.createRun({ ...run, integrationId: this.id, definitionId: run.definition?.id, definitionVersion: run.definition?.version, triggerType: run.trigger?.type, eventId: run.trigger?.eventId });
384
+ } catch (e) {
385
+ try { require('../../logs').getLogger('frigg.data-defined').warn('Workflow run not recorded', { eventName: 'frigg.data-defined.run.not_recorded', integrationId: this.id, workflowId: run.workflowId, cause: e?.message }); } catch { /* ignore */ }
386
+ }
387
+ if (run.status === 'failed') { const e = new Error(run.error); e.run = run; throw e; } // the queue retries
388
+ return run;
389
+ }
390
+
391
+ async runSync(ref) {
392
+ // The sync engine (backfill pages, ongoing deltas, two-way master rules) reuses Frigg's existing SYNC_PAGE
393
+ // pattern; it is specified in README.md and not built yet.
394
+ throw new Error(`Sync ${ref.id} (${ref.phase}) is not implemented yet`);
395
+ }
396
+ }
397
+ Object.defineProperty(DataDefinedIntegration, 'name', { value: `DataDefined_${id.replace(/-/g, '_')}` });
398
+ return DataDefinedIntegration;
399
+ }
400
+
401
+ /** In-memory definition store for tests and local dev. Production: the IntegrationDefinitionVersion table. */
402
+ function memoryDefinitions(docs) {
403
+ const byKey = new Map(docs.map((d) => [`${d.id}@${d.version}`, d]));
404
+ const cmp = (a, b) => a.split('.').map(Number).reduce((r, n, i) => r || n - Number(b.split('.')[i]), 0);
405
+ return {
406
+ get: async (defId, version) => byKey.get(`${defId}@${version}`) || null,
407
+ checksumOf: async (defId, version) => { const doc = byKey.get(`${defId}@${version}`); return doc ? require('./canonical').checksum(doc) : null; },
408
+ list: async () => {
409
+ const latestById = new Map();
410
+ for (const doc of docs) if (!latestById.has(doc.id) || cmp(doc.version, latestById.get(doc.id).version) > 0) latestById.set(doc.id, doc);
411
+ return [...latestById.values()].map((doc) => ({ id: doc.id, version: doc.version, checksum: require('./canonical').checksum(doc), display: doc.display, modules: Object.fromEntries(Object.entries(doc.modules || {}).map(([k, m]) => [k, m.module])), counts: { syncs: (doc.syncs || []).length, links: (doc.links || []).length, workflows: (doc.workflows || []).length, ui: (doc.ui || []).length } }));
412
+ },
413
+ latest: (id) => docs.filter((d) => d.id === id).sort((a, b) => cmp(b.version, a.version))[0] || null,
414
+ };
415
+ }
416
+
417
+ module.exports = { defineIntegrationClass, defineSharedIntegrationClass, memoryDefinitions };