@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.693.d8859c2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/CLAUDE.md +0 -28
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +10 -20
  4. package/application/commands/scheduler-commands.js +7 -11
  5. package/application/commands/user-commands.js +5 -17
  6. package/application/index.js +2 -6
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/repositories/credential-repository-documentdb.js +0 -34
  10. package/credential/repositories/credential-repository-interface.js +0 -15
  11. package/credential/repositories/credential-repository-mongo.js +0 -23
  12. package/credential/repositories/credential-repository-postgres.js +0 -28
  13. package/database/config.js +24 -48
  14. package/database/documentdb-utils.js +0 -2
  15. package/database/encryption/encryption-schema-registry.js +12 -93
  16. package/database/prisma.js +2 -4
  17. package/errors/index.js +4 -0
  18. package/errors/mapping-already-exists-error.js +14 -0
  19. package/generated/prisma-mongodb/edge.js +38 -33
  20. package/generated/prisma-mongodb/index-browser.js +35 -30
  21. package/generated/prisma-mongodb/index.d.ts +1880 -3191
  22. package/generated/prisma-mongodb/index.js +38 -33
  23. package/generated/prisma-mongodb/package.json +1 -1
  24. package/generated/prisma-mongodb/schema.prisma +50 -86
  25. package/generated/prisma-mongodb/wasm.js +38 -33
  26. package/generated/prisma-postgresql/edge.js +38 -33
  27. package/generated/prisma-postgresql/index-browser.js +35 -30
  28. package/generated/prisma-postgresql/index.d.ts +2050 -3390
  29. package/generated/prisma-postgresql/index.js +38 -33
  30. package/generated/prisma-postgresql/package.json +1 -1
  31. package/generated/prisma-postgresql/schema.prisma +48 -81
  32. package/generated/prisma-postgresql/wasm.js +38 -33
  33. package/handlers/WEBHOOKS.md +29 -0
  34. package/handlers/app-definition-loader.js +19 -128
  35. package/handlers/app-handler-helpers.js +3 -25
  36. package/handlers/backend-utils.js +25 -31
  37. package/handlers/middleware/admin-auth.js +1 -14
  38. package/handlers/routers/auth.js +10 -46
  39. package/handlers/routers/health.js +4 -13
  40. package/handlers/routers/integration-defined-routers.js +92 -144
  41. package/handlers/routers/integration-webhook-routers.js +64 -102
  42. package/handlers/workers/integration-defined-workers.js +2 -5
  43. package/index.js +3 -65
  44. package/infrastructure/scheduler/index.js +6 -19
  45. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  46. package/integrations/data-defined/README.md +169 -0
  47. package/integrations/data-defined/canonical.js +11 -0
  48. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  49. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  50. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  51. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  52. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  53. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  54. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  55. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  56. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  57. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  58. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  59. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  60. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  61. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  62. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  63. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  64. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  65. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  66. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  67. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  68. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  69. package/integrations/data-defined/compile.js +77 -0
  70. package/integrations/data-defined/config-form.js +110 -0
  71. package/integrations/data-defined/data-defined-integration.js +366 -0
  72. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  73. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  74. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  75. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  76. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  77. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  78. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  79. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  80. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  81. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  82. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  83. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  84. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  87. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  88. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  89. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  90. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  91. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  92. package/integrations/data-defined/expressions.js +225 -0
  93. package/integrations/data-defined/index.js +39 -0
  94. package/integrations/data-defined/interpreter.js +89 -0
  95. package/integrations/data-defined/manifests.js +125 -0
  96. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  97. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  98. package/integrations/data-defined/router.js +97 -0
  99. package/integrations/data-defined/runtime.js +208 -0
  100. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  101. package/integrations/data-defined/schema/installation.v0.1.schema.json +83 -0
  102. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +848 -0
  103. package/integrations/data-defined/schema/records.md +98 -0
  104. package/integrations/data-defined/schema-check.js +58 -0
  105. package/integrations/data-defined/scripts/validate.js +66 -0
  106. package/integrations/data-defined/steps.js +89 -0
  107. package/integrations/data-defined/transforms.js +91 -0
  108. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  109. package/integrations/data-defined/validate.js +370 -0
  110. package/integrations/data-defined/versioning.js +161 -0
  111. package/integrations/data-defined/webhooks.js +119 -0
  112. package/integrations/index.js +0 -1
  113. package/integrations/integration-base.js +29 -0
  114. package/integrations/integration-router.js +101 -131
  115. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  116. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  117. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  118. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  119. package/integrations/repositories/integration-mapping-repository.js +29 -0
  120. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  121. package/integrations/repositories/integration-repository-interface.js +1 -17
  122. package/integrations/repositories/integration-repository-mongo.js +77 -60
  123. package/integrations/repositories/integration-repository-postgres.js +76 -62
  124. package/integrations/repositories/process-repository-mongo.js +1 -1
  125. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  126. package/integrations/use-cases/create-integration.js +20 -55
  127. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  128. package/integrations/use-cases/index.js +1 -7
  129. package/integrations/utils/map-integration-dto.js +14 -43
  130. package/modules/index.js +0 -14
  131. package/modules/module-factory.js +0 -12
  132. package/modules/module.js +2 -53
  133. package/modules/repositories/module-repository-documentdb.js +6 -51
  134. package/modules/repositories/module-repository-interface.js +2 -19
  135. package/modules/repositories/module-repository-mongo.js +74 -67
  136. package/modules/repositories/module-repository-postgres.js +74 -66
  137. package/modules/repositories/module-repository.js +1 -2
  138. package/modules/requester/requester.js +5 -35
  139. package/modules/use-cases/get-entities-for-user.js +13 -26
  140. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  141. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  142. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  143. package/modules/use-cases/get-module.js +4 -10
  144. package/modules/use-cases/process-authorization-callback.js +19 -53
  145. package/modules/use-cases/refresh-entity-options.js +4 -10
  146. package/modules/use-cases/test-module-auth.js +4 -10
  147. package/package.json +6 -6
  148. package/prisma-mongodb/schema.prisma +50 -86
  149. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  150. package/prisma-postgresql/schema.prisma +48 -81
  151. package/queues/index.js +0 -25
  152. package/types/index.d.ts +0 -2
  153. package/user/repositories/user-repository-documentdb.js +2 -27
  154. package/user/repositories/user-repository-interface.js +0 -18
  155. package/user/repositories/user-repository-mongo.js +0 -24
  156. package/user/repositories/user-repository-postgres.js +0 -18
  157. package/user/use-cases/authenticate-user.js +1 -7
  158. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  159. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  160. package/user/user.js +1 -64
  161. package/credential/use-cases/credential-operations-for-user.js +0 -202
  162. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  163. package/database/organization-where.js +0 -18
  164. package/database/unique-violation.js +0 -25
  165. package/extensions/bootstrap-runner.js +0 -43
  166. package/extensions/extension-loader.js +0 -187
  167. package/extensions/index.js +0 -41
  168. package/extensions/initialize-app.js +0 -123
  169. package/extensions/route-mounter.js +0 -103
  170. package/extensions/schema-composer.js +0 -188
  171. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  172. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  173. package/integrations/repositories/integration-not-found.js +0 -14
  174. package/integrations/schedule-config.js +0 -80
  175. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  176. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  177. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  178. package/management-api/create-management-api-router.js +0 -49
  179. package/management-api/error-response.js +0 -116
  180. package/management-api/management-api-config.js +0 -151
  181. package/management-api/meta-router.js +0 -76
  182. package/management-api/openapi/build-openapi.js +0 -144
  183. package/management-api/openapi/components.js +0 -415
  184. package/management-api/openapi/v1-operations.js +0 -83
  185. package/management-api/openapi/v2-operations.js +0 -264
  186. package/management-api/route-registry.js +0 -411
  187. package/management-api/test-support/http-client.js +0 -39
  188. package/management-api/use-cases/get-management-api-meta.js +0 -57
  189. package/management-api/v1-deprecation.js +0 -120
  190. package/management-api/v2/create-v2-router.js +0 -85
  191. package/management-api/v2/dependencies.js +0 -281
  192. package/management-api/v2/dto.js +0 -143
  193. package/management-api/v2/handlers/authorization.js +0 -94
  194. package/management-api/v2/handlers/credentials.js +0 -33
  195. package/management-api/v2/handlers/entities.js +0 -77
  196. package/management-api/v2/handlers/integrations.js +0 -128
  197. package/management-api/v2/handlers/proxy.js +0 -34
  198. package/management-api/v2/request.js +0 -85
  199. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  200. package/modules/domain/authorization-session.js +0 -144
  201. package/modules/entity-scope.js +0 -15
  202. package/modules/entity-type-registry.js +0 -311
  203. package/modules/proxy/proxy-bases.js +0 -169
  204. package/modules/proxy/proxy-errors.js +0 -31
  205. package/modules/proxy/proxy-path.js +0 -183
  206. package/modules/proxy/proxy-policy.js +0 -277
  207. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  208. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  209. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  210. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  211. package/modules/repositories/entity-not-found.js +0 -18
  212. package/modules/requester/proxy-request.js +0 -245
  213. package/modules/scope-config.js +0 -192
  214. package/modules/scope-policy.js +0 -112
  215. package/modules/scope-resolution.js +0 -288
  216. package/modules/scope-visibility.js +0 -247
  217. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  218. package/modules/tests/doubles/ownership-matching.js +0 -18
  219. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  220. package/modules/use-cases/create-entity-from-credential.js +0 -113
  221. package/modules/use-cases/entity-operations-for-user.js +0 -162
  222. package/modules/use-cases/entity-types.js +0 -139
  223. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  224. package/modules/use-cases/get-authorization-requirements.js +0 -100
  225. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  226. package/modules/use-cases/manage-access.js +0 -21
  227. package/modules/use-cases/multi-step-authorization.js +0 -329
  228. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  229. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  230. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  231. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  232. package/providers/index.js +0 -23
  233. package/providers/resolve-provider.js +0 -93
  234. package/providers/runtime-detection.js +0 -124
  235. package/queues/providers/index.js +0 -11
  236. package/queues/providers/netlify-background-provider.js +0 -150
  237. package/queues/providers/qstash-queue-provider.js +0 -219
  238. package/queues/providers/sqs-queue-provider.js +0 -70
  239. package/queues/queue-client-interface.js +0 -55
  240. package/queues/queue-message-auth.js +0 -193
  241. package/queues/queue-provider-factory.js +0 -101
  242. package/queues/queue-provider.js +0 -65
  243. package/queues/sqs-queue-client.js +0 -81
  244. package/types/modules/index.d.ts +0 -214
  245. package/types/user/index.d.ts +0 -134
  246. package/user/adopter-jwt-config.js +0 -177
  247. package/user/adopter-jwt-errors.js +0 -59
  248. package/user/adopter-token-verifier.js +0 -122
  249. package/user/app-user-helpers.js +0 -97
  250. package/user/create-user-authentication.js +0 -64
  251. package/user/membership.js +0 -51
  252. package/user/repositories/organization-membership-record.js +0 -19
  253. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  254. package/user/repositories/organization-membership-repository-factory.js +0 -42
  255. package/user/repositories/organization-membership-repository-interface.js +0 -57
  256. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  257. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  258. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  259. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  260. package/user/use-cases/ensure-organization-membership.js +0 -92
  261. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -0,0 +1,98 @@
1
+ # Data-defined integrations: database records (v0.1)
2
+
3
+ Three layers, stored separately. Worked examples for Phone ↔ HubSpot are in `examples/records/`.
4
+
5
+ | Layer | What it holds | Table | Changes when |
6
+ |---|---|---|---|
7
+ | Definition | What the integration can do | `IntegrationDefinitionVersion` (new) | A version is published (then never again) |
8
+ | Installation | What one customer turned on and chose | `Integration.config` (existing column, new shape) | The customer configures, or an upgrade moves the pin |
9
+ | State | What has happened | `IntegrationMapping` (existing), `WorkflowRun` (new) | Every event |
10
+
11
+ ## IntegrationDefinitionVersion (new)
12
+
13
+ One row per published version. `document` is the definition verbatim; `checksum` is sha256 of its canonical JSON
14
+ (sorted keys), so the same content always has the same checksum and an installation can prove what it runs.
15
+
16
+ ```prisma
17
+ model IntegrationDefinitionVersion {
18
+ id String @id @default(cuid())
19
+ definitionId String // "phone-hubspot"
20
+ version String // "1.4.0" (semver)
21
+ schemaVersion String // "0.1"
22
+ status DefinitionStatus @default(DRAFT)
23
+ checksum String // "sha256:..." of canonical(document)
24
+ document Json // the definition, immutable once PUBLISHED
25
+ changeLevel String? // patch | minor | major, computed by versioning.diff at publish
26
+ previousVersion String?
27
+ notes String?
28
+ createdBy String?
29
+ createdAt DateTime @default(now())
30
+ publishedAt DateTime?
31
+
32
+ @@unique([definitionId, version])
33
+ @@index([definitionId, status])
34
+ }
35
+
36
+ enum DefinitionStatus { DRAFT PUBLISHED DEPRECATED RETIRED }
37
+ ```
38
+
39
+ Rules: a `DRAFT` may be edited; publishing runs `validate` (must be valid) and `checkBump` against the latest
40
+ published version (the declared bump must cover the computed change level), then freezes the row. `DEPRECATED`
41
+ versions keep running for installations pinned to them but cannot be newly installed. A version is `RETIRED` only
42
+ when no installation pins it and no queued message references it.
43
+
44
+ ## Integration.config (existing column)
45
+
46
+ ```jsonc
47
+ {
48
+ "definition": { "id": "phone-hubspot", "version": "1.4.0", "checksum": "sha256:…" }, // the pin
49
+ "settings": { "fromNumber": "+15125550100" }, // integration-level settings
50
+ "syncs": { "contacts": { "enabled": true, "mapping": [ /* optional full override */ ] } },
51
+ "workflows": { "missed-call-task": { "enabled": true, "settings": { "owner": "81273645" } } },
52
+ "ui": { "phone-card": { "enabled": true } },
53
+ "runtime": { "installed": { "hook": { "id": "WHk2c9", "url": "…", "events": [ … ] } } } // runtime-owned: onInstall results by step id
54
+ }
55
+ ```
56
+
57
+ An absent `enabled` means "use the definition's default". The existing `Integration.version` column mirrors
58
+ `config.definition.version` so it can be queried and indexed.
59
+
60
+ ## IntegrationMapping (existing) — three kinds of row
61
+
62
+ | `sourceId` | `mapping` | Written by |
63
+ |---|---|---|
64
+ | `link:<sync>:<moduleKey>:<id>` | `{ "<moduleKey>": id, …, linkedAt }` | `links.upsert` (both sides get a row) |
65
+ | `idx:<sync>:<identityKey>:<normalised value>` | same as link | `links.upsert` with `keys` (email lowercased, phone E.164) |
66
+ | `evt:<definition>:<workflow>:<providerEventId>` | `{ at }` | `runWorkflow`, insert-only (`createMapping`) so a redelivery is a no-op |
67
+
68
+ No new table: these are the rows hand-written integrations already keep, under a fixed naming scheme.
69
+
70
+ ## WorkflowRun (new)
71
+
72
+ One row per workflow execution. Carries the definition version it ran on, so a run is always explainable against the
73
+ exact document, even after an upgrade.
74
+
75
+ ```prisma
76
+ model WorkflowRun {
77
+ id String @id @default(cuid())
78
+ integrationId String
79
+ definitionId String
80
+ definitionVersion String
81
+ workflowId String
82
+ triggerType String // webhook | schedule | userAction | syncRecord
83
+ eventId String? // provider event id (dedupe key)
84
+ status String // completed | stopped | filtered | duplicate | failed
85
+ reason String? // why it stopped
86
+ error String?
87
+ attempt Int @default(1)
88
+ trace Json // [{ id, use, status, ms, error? }] — no payload bodies
89
+ startedAt DateTime
90
+ endedAt DateTime?
91
+
92
+ @@index([integrationId, workflowId, startedAt])
93
+ @@index([integrationId, status])
94
+ }
95
+ ```
96
+
97
+ Retention: keep `failed` runs 30 days and the rest 7 by default. `trace` holds step ids, timings and error messages,
98
+ never request or response bodies (those can hold customer data; the existing logging rules apply).
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Two layers of validation, run together everywhere (the CLI, the tests, anything that publishes):
3
+ * 1. JSON Schema (schema/*.schema.json) — the published shape. Needs ajv, which the monorepo installs for
4
+ * @friggframework/schemas; core's runtime stays dependency-free, so ajv is loaded only here.
5
+ * 2. References (validate.js / manifests.js) — what a schema cannot see: steps name declared capabilities,
6
+ * $.steps.x is an earlier step, view paths exist, option inputs are declared, and so on.
7
+ * A document is valid only when both pass.
8
+ */
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { validate } = require('./validate');
12
+ const { validateManifest } = require('./manifests');
13
+
14
+ const SCHEMA_DIR = path.join(__dirname, 'schema');
15
+ let compiled;
16
+
17
+ function loadAjv() {
18
+ try { return require('ajv/dist/2020'); } catch {
19
+ const e = new Error('ajv is not installed. Run npm install at the monorepo root (@friggframework/schemas depends on ajv@8).');
20
+ e.code = 'NO_AJV';
21
+ throw e;
22
+ }
23
+ }
24
+
25
+ function schemas() {
26
+ if (compiled) return compiled;
27
+ const Ajv = loadAjv();
28
+ const ajv = new Ajv({ allErrors: true, strict: false });
29
+ const read = (f) => JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, f), 'utf8'));
30
+ compiled = {
31
+ definition: ajv.compile(read('integration-definition.v0.1.schema.json')),
32
+ manifest: ajv.compile(read('capability-manifest.v0.1.schema.json')),
33
+ installation: ajv.compile(read('installation.v0.1.schema.json')),
34
+ };
35
+ return compiled;
36
+ }
37
+
38
+ const schemaErrors = (check, doc) => (check(doc) ? [] : check.errors.map((e) => ({ layer: 'schema', path: e.instancePath.replace(/^\//, '').replace(/\//g, '.') || '(root)', message: e.message + (e.params?.allowedValues ? ` (${e.params.allowedValues.join(', ')})` : e.params?.additionalProperty ? ` ("${e.params.additionalProperty}")` : '') })));
39
+
40
+ /** @returns {{ valid: boolean, errors: Array<{layer,path,message}>, warnings: Array<{layer,path,message}> }} */
41
+ function checkManifest(manifest) {
42
+ const errors = [...schemaErrors(schemas().manifest, manifest), ...validateManifest(manifest).errors.map((e) => ({ layer: 'references', ...e }))];
43
+ return { valid: errors.length === 0, errors, warnings: [] };
44
+ }
45
+
46
+ /** A definition against the manifests of the modules it uses. Both layers run even when the first fails. */
47
+ function checkDefinition(def, manifests) {
48
+ const v = validate(def, manifests);
49
+ const errors = [...schemaErrors(schemas().definition, def), ...v.errors.map((e) => ({ layer: 'references', ...e }))];
50
+ return { valid: errors.length === 0, errors, warnings: v.warnings.map((w) => ({ layer: 'references', ...w })) };
51
+ }
52
+
53
+ function checkInstallation(config) {
54
+ const errors = schemaErrors(schemas().installation, config);
55
+ return { valid: errors.length === 0, errors, warnings: [] };
56
+ }
57
+
58
+ module.exports = { checkDefinition, checkManifest, checkInstallation, loadAjv };
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate data-defined integration definitions and capability manifests: JSON Schema, then references.
4
+ *
5
+ * node scripts/validate.js every bundled manifest and example
6
+ * node scripts/validate.js my.definition.json one or more definitions (files or directories)
7
+ * node scripts/validate.js --manifests dir/ ... check against manifests in another directory
8
+ * node scripts/validate.js --strict ... warnings fail too
9
+ * node scripts/validate.js --json ... machine-readable result on stdout
10
+ *
11
+ * Exit codes: 0 valid · 1 invalid · 2 could not run (bad arguments, unreadable file, ajv missing).
12
+ * Manifests are always checked first, because a definition is only as trustworthy as the manifests it is checked
13
+ * against: an invalid manifest fails the run even if every definition passes.
14
+ */
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const root = path.join(__dirname, '..');
19
+ const args = process.argv.slice(2);
20
+ const flag = (name) => { const i = args.indexOf(name); if (i < 0) return false; args.splice(i, 1); return true; };
21
+ const option = (name) => { const i = args.indexOf(name); if (i < 0) return null; const v = args[i + 1]; args.splice(i, 2); return v; };
22
+
23
+ const strict = flag('--strict');
24
+ const asJson = flag('--json');
25
+ const manifestDir = option('--manifests') || path.join(root, 'capabilities');
26
+ const unknown = args.find((a) => a.startsWith('--'));
27
+ if (unknown) { console.error(`Unknown option ${unknown}`); process.exit(2); }
28
+
29
+ const jsonFiles = (p) => {
30
+ if (!fs.existsSync(p)) { console.error(`No such file or directory: ${p}`); process.exit(2); }
31
+ return fs.statSync(p).isDirectory() ? fs.readdirSync(p).filter((f) => f.endsWith('.json')).map((f) => path.join(p, f)) : [p];
32
+ };
33
+ const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (e) { console.error(`${f}: not valid JSON (${e.message})`); process.exit(2); } };
34
+
35
+ let check;
36
+ try { check = require('../schema-check'); check.loadAjv(); } catch (e) { console.error(e.message); process.exit(2); }
37
+
38
+ const manifestFiles = jsonFiles(manifestDir).filter((f) => f.endsWith('.capabilities.json'));
39
+ const definitionFiles = (args.length ? args : [path.join(root, 'examples')]).flatMap(jsonFiles).filter((f) => !f.endsWith('.capabilities.json') && !f.includes(`${path.sep}records${path.sep}`));
40
+
41
+ const manifests = {};
42
+ const results = [];
43
+ for (const f of manifestFiles) {
44
+ const m = readJson(f);
45
+ manifests[m.module] = m;
46
+ results.push({ kind: 'manifest', file: path.relative(process.cwd(), f), name: m.module, ...check.checkManifest(m) });
47
+ }
48
+ for (const f of definitionFiles) {
49
+ const def = readJson(f);
50
+ results.push({ kind: 'definition', file: path.relative(process.cwd(), f), name: `${def.id}@${def.version}`, ...check.checkDefinition(def, manifests) });
51
+ }
52
+
53
+ const failed = results.filter((r) => !r.valid || (strict && r.warnings.length));
54
+ if (asJson) {
55
+ process.stdout.write(JSON.stringify({ valid: failed.length === 0, strict, results }, null, 2) + '\n');
56
+ } else {
57
+ for (const r of results) {
58
+ const bad = !r.valid || (strict && r.warnings.length);
59
+ console.log(`${bad ? 'FAIL' : 'ok '} ${r.kind.padEnd(10)} ${r.name}${r.warnings.length ? ` (${r.warnings.length} warning${r.warnings.length > 1 ? 's' : ''})` : ''}`);
60
+ for (const e of r.errors) console.log(` error [${e.layer}] ${e.path}: ${e.message}`);
61
+ for (const w of r.warnings) console.log(` warning [${w.layer}] ${w.path}: ${w.message}`);
62
+ }
63
+ const n = (k) => results.filter((r) => r.kind === k).length;
64
+ console.log(`\n${n('manifest')} manifests, ${n('definition')} definitions: ${failed.length ? `${failed.length} failed` : 'all valid'}${strict ? ' (strict)' : ''}`);
65
+ }
66
+ process.exit(failed.length ? 1 : 0);
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The v0.1 step catalogue. A step is { id?, use, with?, when?, onError? }.
3
+ *
4
+ * `use` is either a built-in below or "<moduleKey>.<object>.<verb>": a capability the module's manifest declares
5
+ * (e.g. "hubspot.task.create" -> HubSpot Api.createTask). Nothing else can run: there is no http, eval or script step.
6
+ * The only way to run custom code is `code`, which calls a named, versioned function registered with the runtime.
7
+ */
8
+ const { applyMapping, deepMerge } = require('./transforms');
9
+ const { resolve } = require('./expressions');
10
+
11
+ const BUILTINS = {
12
+ 'links.find': {
13
+ summary: 'Find records already linked, by a sync or a link set (definition.links). By one side\'s id, or by an identity key it indexes (email, phone).',
14
+ args: { link: 'link set id (or sync: sync id)', from: 'module key, with id', id: 'that module\'s record id', key: '{ <identity field>: value }' },
15
+ run: (a, { ports }) => ports.links.find(a),
16
+ },
17
+ 'links.upsert': {
18
+ summary: 'Record that records on two sides are the same thing, and index their identity keys.',
19
+ args: { link: 'link set id (or sync: sync id)', ids: '{ <module key>: id }', keys: '{ <identity field>: value }' },
20
+ run: (a, { ports }) => ports.links.upsert(a),
21
+ },
22
+ map: {
23
+ summary: 'Apply a field mapping: a sync\'s mapping (with the installation\'s overrides) or an inline one.',
24
+ args: { sync: 'sync id', mapping: 'inline rows', record: 'the source record' },
25
+ run: (a, { def, installation }) => {
26
+ let rows = a.mapping;
27
+ if (!rows && a.sync) {
28
+ const sync = (def.syncs || []).find((s) => s.id === a.sync);
29
+ if (!sync) throw new Error(`map: unknown sync "${a.sync}"`);
30
+ rows = installation?.syncs?.[a.sync]?.mapping || sync.mapping;
31
+ }
32
+ return applyMapping(rows, a.record || {});
33
+ },
34
+ },
35
+ template: {
36
+ summary: 'Render a named template (definition.templates) or inline text.',
37
+ args: { id: 'template id', text: 'inline template' },
38
+ run: (a, { def, ctx }) => {
39
+ const src = a.id ? def.templates?.[a.id] : a.text;
40
+ if (src == null) throw new Error(`template: unknown template "${a.id}"`);
41
+ return resolve(src, ctx);
42
+ },
43
+ },
44
+ merge: {
45
+ summary: 'Deep-merge objects into one; later objects win, lists are replaced.',
46
+ args: { objects: '[object, ...]' },
47
+ run: (a) => deepMerge(...(Array.isArray(a.objects) ? a.objects : [])),
48
+ },
49
+ set: {
50
+ summary: 'Store values in $.vars for later steps.',
51
+ args: { '<name>': 'value' },
52
+ run: (a, { ctx }) => { Object.assign(ctx.vars, a); return a; },
53
+ },
54
+ stop: {
55
+ summary: 'End the run early (a normal outcome, not an error). Usually guarded by `when`.',
56
+ args: { reason: 'string' },
57
+ run: (a) => ({ stop: true, reason: a.reason || 'stopped' }),
58
+ },
59
+ foreach: {
60
+ summary: 'Run nested steps once per item ($.item). Bounded by max (default 100, hard cap 1000). With `collect`, the step\'s result is the list of that value per item; `where` skips items.',
61
+ args: { over: 'array', max: 'number', where: 'condition on $.item', collect: 'value per item (may use $.item and this iteration\'s $.steps)', do: 'steps (optional when collect is set)' },
62
+ nested: true,
63
+ },
64
+ branch: {
65
+ summary: 'Run the first case whose `when` holds, else the `else` steps.',
66
+ args: { cases: '[{ when, do }]', else: 'steps' },
67
+ nested: true,
68
+ },
69
+ code: {
70
+ summary: 'Call a registered code step: a named, versioned function shipped in reviewed code ("pkg#fn@1.2.0").',
71
+ args: { ref: 'string', with: 'object' },
72
+ run: (a, { ports }) => {
73
+ if (!ports.runCode) throw new Error('code: this runtime has no code steps registered');
74
+ return ports.runCode(a.ref, a.with || {});
75
+ },
76
+ },
77
+ };
78
+
79
+ const FOREACH_HARD_CAP = 1000;
80
+
81
+ function isBuiltin(use) { return Object.prototype.hasOwnProperty.call(BUILTINS, use); }
82
+
83
+ /** "hubspot.task.create" -> { module: 'hubspot', capability: 'task.create' } */
84
+ function splitCapability(use) {
85
+ const i = use.indexOf('.');
86
+ return i < 0 ? null : { module: use.slice(0, i), capability: use.slice(i + 1) };
87
+ }
88
+
89
+ module.exports = { BUILTINS, FOREACH_HARD_CAP, isBuiltin, splitCapability };
@@ -0,0 +1,91 @@
1
+ /** Field transforms used by sync mappings and the `map` step (v0.1). Pure functions: (value, options, record) -> value. */
2
+ const TRANSFORMS = {
3
+ none: (v) => v,
4
+ trim: (v) => (typeof v === 'string' ? v.trim() : v),
5
+ lowercase: (v) => (typeof v === 'string' ? v.toLowerCase() : v),
6
+ uppercase: (v) => (typeof v === 'string' ? v.toUpperCase() : v),
7
+ number: (v) => (v == null || v === '' ? undefined : Number(v)),
8
+ boolean: (v) => (v == null ? undefined : v === true || v === 'true' || v === 1 || v === '1'),
9
+ to_iso_date: (v) => { if (!v) return undefined; const d = new Date(v); return isNaN(d) ? undefined : d.toISOString(); },
10
+ /** Digits with a leading +. A 10-digit number is assumed to be NANP (options.defaultCountry overrides). */
11
+ to_e164_phone: (v, o = {}) => {
12
+ if (v == null || v === '') return undefined;
13
+ const s = String(v).trim();
14
+ const digits = s.replace(/\D/g, '');
15
+ if (!digits) return undefined;
16
+ if (s.startsWith('+')) return '+' + digits;
17
+ if (digits.length === 10) return (o.defaultCountry === 'GB' ? '+44' : '+1') + digits;
18
+ if (digits.length === 11 && digits.startsWith('1')) return '+' + digits;
19
+ return '+' + digits;
20
+ },
21
+ split_full_name: (v, o = {}) => {
22
+ if (!v) return undefined;
23
+ const parts = String(v).trim().split(/\s+/);
24
+ return o.part === 'last' ? parts.slice(1).join(' ') || undefined : parts[0];
25
+ },
26
+ join: (v, o = {}, record = {}) => (o.fields || []).map((f) => readField(record, f)).filter((x) => x != null && x !== '').join(o.separator ?? ' ') || undefined,
27
+ map_values: (v, o = {}) => (v == null ? undefined : (o.values || {})[v] ?? o.fallback),
28
+ default: (v, o = {}) => (v == null || v === '' ? o.value : v),
29
+ // money and time, rounded and total (bad input -> undefined, which the mapping leaves out)
30
+ units_to_cents: (v) => { const n = v == null || v === '' ? NaN : Number(v); return Number.isNaN(n) ? undefined : Math.round(Number(`${n}e2`)); },
31
+ cents_to_units: (v) => { const n = v == null || v === '' ? NaN : Number(v); return Number.isNaN(n) ? undefined : Math.round(n) / 100; },
32
+ from_unix_seconds: (v) => { const n = v == null || v === '' ? NaN : Number(v); return Number.isNaN(n) ? undefined : new Date(n * 1000).toISOString(); },
33
+ to_unix_seconds: (v) => { const t = v ? Date.parse(v) : NaN; return Number.isNaN(t) ? undefined : Math.floor(t / 1000); },
34
+ };
35
+
36
+ function readField(record, path) {
37
+ let cur = record;
38
+ for (const seg of String(path).split(/\.|(?=\[)/)) {
39
+ if (cur == null) return undefined;
40
+ const m = /^\[(\d+)\]$/.exec(seg);
41
+ cur = m ? cur[Number(m[1])] : cur[seg];
42
+ }
43
+ return cur;
44
+ }
45
+
46
+ function writeField(target, path, value) {
47
+ const segs = String(path).split(/\.|(?=\[)/);
48
+ let cur = target;
49
+ segs.forEach((seg, i) => {
50
+ const m = /^\[(\d+)\]$/.exec(seg);
51
+ const key = m ? Number(m[1]) : seg;
52
+ if (i === segs.length - 1) { cur[key] = value; return; }
53
+ const nextIsIndex = /^\[\d+\]$/.test(segs[i + 1]);
54
+ if (cur[key] == null) cur[key] = nextIsIndex ? [] : {};
55
+ cur = cur[key];
56
+ });
57
+ return target;
58
+ }
59
+
60
+ /**
61
+ * Apply a field mapping to a record. A blank result is left out, never written (a blank must not overwrite a value).
62
+ * A row either reads `source` from the record or writes a fixed `value` (QuickBooks' Line[0].DetailType, a lifecycle
63
+ * stage): { target, value }. A fixed value still goes through the row's transform.
64
+ * @param {Array<{source?:string,value?:any,target:string,transform?:string,options?:object}>} rows
65
+ */
66
+ function applyMapping(rows, record) {
67
+ const out = {};
68
+ for (const r of rows || []) {
69
+ const fn = TRANSFORMS[r.transform || 'none'];
70
+ if (!fn) throw new Error(`Unknown transform "${r.transform}"`);
71
+ const input = 'value' in r ? r.value : readField(record, r.source);
72
+ const v = fn(input, r.options || {}, record);
73
+ if (v === undefined || v === null || v === '') continue;
74
+ writeField(out, r.target, v);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /** Deep merge, later objects win; arrays and scalars are replaced, not merged. Inputs are not modified. */
80
+ function deepMerge(...objects) {
81
+ const out = {};
82
+ for (const o of objects) {
83
+ if (!o || typeof o !== 'object' || Array.isArray(o)) continue;
84
+ for (const [k, v] of Object.entries(o)) {
85
+ out[k] = v && typeof v === 'object' && !Array.isArray(v) && out[k] && typeof out[k] === 'object' && !Array.isArray(out[k]) ? deepMerge(out[k], v) : v;
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+
91
+ module.exports = { TRANSFORMS, applyMapping, readField, writeField, deepMerge };
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Application layer for data-defined integrations (ADR-063): publishing definition versions, the catalogue the
3
+ * runtime reads from, and per-installation upgrades. Business rules live here; the repositories only store rows.
4
+ *
5
+ * createDefinitionCatalog() what defineSharedIntegrationClass({ definitions }) reads: get(id, version), latest(id)
6
+ * PublishDefinitionVersion validate (schema + references) → checkBump against the latest published → store
7
+ * DeprecateDefinitionVersion PUBLISHED → DEPRECATED (still runs for its pins, cannot be newly installed)
8
+ * ListPublishedDefinitions the install catalogue: latest published version of each definition
9
+ * GetDefinitionHistory every version of one definition, newest first
10
+ * PlanInstallationUpgrade planUpgrade(installation, pinned, target), without changing anything
11
+ */
12
+ const { checksum } = require('../canonical');
13
+ const { checkBump, planUpgrade } = require('../versioning');
14
+
15
+ class DefinitionError extends Error {
16
+ constructor(message, { status = 400, code, details } = {}) { super(message); this.name = 'DefinitionError'; this.status = status; this.code = code; this.details = details; }
17
+ }
18
+
19
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$/;
20
+ function compareVersions(a, b) {
21
+ const x = SEMVER.exec(a), y = SEMVER.exec(b);
22
+ if (!x || !y) return String(a).localeCompare(String(b));
23
+ for (let i = 1; i <= 3; i += 1) if (Number(x[i]) !== Number(y[i])) return Number(x[i]) - Number(y[i]);
24
+ if (x[4] && !y[4]) return -1; // 1.0.0-beta < 1.0.0
25
+ if (!x[4] && y[4]) return 1;
26
+ return String(x[4] || '').localeCompare(String(y[4] || ''));
27
+ }
28
+ const newestFirst = (rows) => [...rows].sort((a, b) => compareVersions(b.version, a.version));
29
+ const SERVABLE = new Set(['PUBLISHED', 'DEPRECATED', 'RETIRED']); // a DRAFT never reaches the runtime
30
+
31
+ /**
32
+ * The definition store the runtime reads from. Published rows are immutable, so they are cached by id@version for the
33
+ * life of the process; a pinned checksum is verified, so an installation provably runs the document it was pinned to.
34
+ */
35
+ function createDefinitionCatalog({ repository } = {}) {
36
+ const repo = () => repository || (repository = require('../repositories/definition-version-repository').createDefinitionVersionRepository());
37
+ const cache = new Map();
38
+ return {
39
+ async get(definitionId, version, { checksum: expected } = {}) {
40
+ const key = `${definitionId}@${version}`;
41
+ let row = cache.get(key);
42
+ if (!row) {
43
+ row = await repo().findVersion(definitionId, version);
44
+ if (!row || !SERVABLE.has(row.status)) return null;
45
+ cache.set(key, row);
46
+ }
47
+ if (expected && expected !== row.checksum) throw new DefinitionError(`${key} does not match the pinned checksum`, { status: 409, code: 'CHECKSUM_MISMATCH' });
48
+ return row.document;
49
+ },
50
+ /** The latest PUBLISHED version (not deprecated, not draft): what a new installation pins. */
51
+ async latest(definitionId) {
52
+ const rows = (await repo().listVersions(definitionId)).filter((r) => r.status === 'PUBLISHED');
53
+ return rows.length ? newestFirst(rows)[0].document : null;
54
+ },
55
+ async checksumOf(definitionId, version) {
56
+ return (await repo().findVersion(definitionId, version))?.checksum || null;
57
+ },
58
+ /** The install catalogue: latest published version of each definition. */
59
+ list: () => new ListPublishedDefinitions({ repository: repo() }).execute(),
60
+ /** The repository behind this catalogue, for publishing (bundled stores have none). */
61
+ get repository() { return repo(); },
62
+ };
63
+ }
64
+
65
+ class PublishDefinitionVersion {
66
+ /**
67
+ * @param {{ repository, manifests: object, check?: (def, manifests) => { valid, errors, warnings } }} deps
68
+ * check defaults to schema-check's checkDefinition (JSON Schema + references)
69
+ */
70
+ constructor({ repository, manifests, check }) {
71
+ this.repository = repository;
72
+ this.manifests = manifests;
73
+ this.check = check || require('../schema-check').checkDefinition;
74
+ }
75
+
76
+ /** @returns {Promise<{ version: object, created: boolean, changeLevel: string|null, warnings: array }>} */
77
+ async execute({ document, notes, createdBy, publish = true, strict = false } = {}) {
78
+ if (!document || typeof document !== 'object') throw new DefinitionError('Send the definition as `document`');
79
+ const result = this.check(document, this.manifests);
80
+ if (!result.valid || (strict && result.warnings.length)) throw new DefinitionError(`${document.id}@${document.version} is not valid`, { code: 'INVALID', details: { errors: result.errors, warnings: result.warnings } });
81
+
82
+ const sum = checksum(document);
83
+ const existing = await this.repository.findVersion(document.id, document.version);
84
+ if (existing && existing.status !== 'DRAFT') {
85
+ if (existing.checksum === sum) return { version: existing, created: false, changeLevel: existing.changeLevel, warnings: result.warnings };
86
+ throw new DefinitionError(`${document.id}@${document.version} is already ${existing.status.toLowerCase()} and cannot change. Publish a new version.`, { status: 409, code: 'IMMUTABLE' });
87
+ }
88
+
89
+ const published = newestFirst((await this.repository.listVersions(document.id)).filter((r) => r.status === 'PUBLISHED' || r.status === 'DEPRECATED'));
90
+ const previous = published.find((r) => r.version !== document.version) || null;
91
+ let changeLevel = null;
92
+ if (previous) {
93
+ if (compareVersions(document.version, previous.version) <= 0) throw new DefinitionError(`${document.version} is not newer than the latest published ${previous.version}`, { status: 409, code: 'NOT_NEWER' });
94
+ const bump = checkBump(previous.document, document);
95
+ if (!bump.ok) throw new DefinitionError(bump.message, { status: 409, code: 'BUMP_TOO_SMALL', details: { required: bump.required, declared: bump.declared, changes: bump.changes } });
96
+ changeLevel = bump.required;
97
+ }
98
+
99
+ const row = {
100
+ definitionId: document.id, version: document.version, schemaVersion: document.schemaVersion, checksum: sum, document,
101
+ changeLevel, previousVersion: previous?.version || null, notes: notes ?? null, createdBy: createdBy ?? null,
102
+ status: publish ? 'PUBLISHED' : 'DRAFT', publishedAt: publish ? new Date() : null,
103
+ };
104
+ if (existing) {
105
+ // A draft may be rewritten, and published, as long as it is still a draft when we write (checked by the
106
+ // database, so two concurrent publishes cannot both win).
107
+ const updated = await this.repository.updateIfStatus(document.id, document.version, 'DRAFT', row);
108
+ if (!updated) throw new DefinitionError(`${document.id}@${document.version} changed while publishing; try again`, { status: 409, code: 'CONFLICT' });
109
+ return { version: updated, created: false, changeLevel, warnings: result.warnings };
110
+ }
111
+ try {
112
+ return { version: await this.repository.createVersion(row), created: true, changeLevel, warnings: result.warnings };
113
+ } catch (e) {
114
+ if (e?.code === 'P2002') {
115
+ // Lost a race to the same version: the same document is the same publish; anything else is a conflict.
116
+ const winner = await this.repository.findVersion(document.id, document.version);
117
+ if (winner?.checksum === sum) return { version: winner, created: false, changeLevel: winner.changeLevel, warnings: result.warnings };
118
+ throw new DefinitionError(`${document.id}@${document.version} was published with different content just now`, { status: 409, code: 'CONFLICT' });
119
+ }
120
+ throw e;
121
+ }
122
+ }
123
+ }
124
+
125
+ class DeprecateDefinitionVersion {
126
+ constructor({ repository }) { this.repository = repository; }
127
+ async execute({ definitionId, version }) {
128
+ const row = await this.repository.updateIfStatus(definitionId, version, 'PUBLISHED', { status: 'DEPRECATED' });
129
+ if (row) return row;
130
+ const existing = await this.repository.findVersion(definitionId, version);
131
+ if (!existing) throw new DefinitionError(`${definitionId}@${version} does not exist`, { status: 404, code: 'NOT_FOUND' });
132
+ throw new DefinitionError(`${definitionId}@${version} is ${existing.status.toLowerCase()}; only a published version can be deprecated`, { status: 409, code: 'WRONG_STATUS' });
133
+ }
134
+ }
135
+
136
+ class ListPublishedDefinitions {
137
+ constructor({ repository }) { this.repository = repository; }
138
+ /** Latest published version of each definition, with what an install screen shows. */
139
+ async execute() {
140
+ const byId = new Map();
141
+ for (const r of await this.repository.listByStatus(['PUBLISHED'])) {
142
+ const cur = byId.get(r.definitionId);
143
+ if (!cur || compareVersions(r.version, cur.version) > 0) byId.set(r.definitionId, r);
144
+ }
145
+ const out = [];
146
+ for (const r of byId.values()) {
147
+ const doc = (await this.repository.findVersion(r.definitionId, r.version)).document;
148
+ out.push({
149
+ id: r.definitionId, version: r.version, checksum: r.checksum, publishedAt: r.publishedAt,
150
+ display: doc.display, modules: Object.fromEntries(Object.entries(doc.modules || {}).map(([k, m]) => [k, m.module])),
151
+ counts: { syncs: (doc.syncs || []).length, links: (doc.links || []).length, workflows: (doc.workflows || []).length, ui: (doc.ui || []).length },
152
+ });
153
+ }
154
+ return out.sort((a, b) => String(a.display?.label || a.id).localeCompare(String(b.display?.label || b.id)));
155
+ }
156
+ }
157
+
158
+ class GetDefinitionHistory {
159
+ constructor({ repository }) { this.repository = repository; }
160
+ async execute({ definitionId }) {
161
+ const rows = newestFirst(await this.repository.listVersions(definitionId));
162
+ if (!rows.length) throw new DefinitionError(`No definition "${definitionId}"`, { status: 404, code: 'NOT_FOUND' });
163
+ return rows.map(({ document, ...meta }) => meta);
164
+ }
165
+ }
166
+
167
+ class PlanInstallationUpgrade {
168
+ constructor({ catalog }) { this.catalog = catalog; }
169
+ /** @returns {Promise<{ from, to, allowed, needs, actions, config, changes }>} */
170
+ async execute({ config, toVersion }) {
171
+ const pin = config?.definition;
172
+ if (!pin?.id || !pin?.version) throw new DefinitionError('This installation does not name a definition', { code: 'NO_PIN' });
173
+ const from = await this.catalog.get(pin.id, pin.version);
174
+ const target = toVersion || (await this.catalog.latest(pin.id))?.version;
175
+ if (!target) throw new DefinitionError(`No published version of "${pin.id}"`, { status: 404, code: 'NOT_FOUND' });
176
+ const to = await this.catalog.get(pin.id, target);
177
+ if (!from || !to) throw new DefinitionError(`${pin.id}@${!from ? pin.version : target} is not published`, { status: 404, code: 'NOT_FOUND' });
178
+ const plan = planUpgrade(config, from, to);
179
+ plan.config.definition = { ...plan.config.definition, checksum: await this.catalog.checksumOf(pin.id, target) };
180
+ return { from: pin.version, to: target, ...plan, changes: require('../versioning').diff(from, to).changes };
181
+ }
182
+ }
183
+
184
+ module.exports = {
185
+ DefinitionError, compareVersions, createDefinitionCatalog,
186
+ PublishDefinitionVersion, DeprecateDefinitionVersion, ListPublishedDefinitions, GetDefinitionHistory, PlanInstallationUpgrade,
187
+ };