@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.686.e94de4d.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 (207) hide show
  1. package/CLAUDE.md +28 -40
  2. package/README.md +105 -0
  3. package/application/commands/entity-commands.js +60 -18
  4. package/application/commands/integration-commands.js +20 -73
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-encryption-service.js +9 -22
  18. package/database/documentdb-utils.js +2 -0
  19. package/database/encryption/README.md +76 -14
  20. package/database/encryption/documentdb-encryption-service.md +78 -1
  21. package/database/encryption/encryption-config.js +219 -0
  22. package/database/encryption/encryption-schema-registry.js +93 -12
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/organization-where.js +18 -0
  25. package/database/prisma.js +15 -38
  26. package/database/unique-violation.js +25 -0
  27. package/database/use-cases/check-encryption-health-use-case.js +43 -52
  28. package/errors/index.js +0 -4
  29. package/extensions/bootstrap-runner.js +43 -0
  30. package/extensions/extension-loader.js +187 -0
  31. package/extensions/index.js +41 -0
  32. package/extensions/initialize-app.js +123 -0
  33. package/extensions/route-mounter.js +103 -0
  34. package/extensions/schema-composer.js +188 -0
  35. package/generated/prisma-mongodb/edge.js +43 -4
  36. package/generated/prisma-mongodb/index-browser.js +40 -1
  37. package/generated/prisma-mongodb/index.d.ts +4761 -133
  38. package/generated/prisma-mongodb/index.js +43 -4
  39. package/generated/prisma-mongodb/package.json +1 -1
  40. package/generated/prisma-mongodb/schema.prisma +92 -0
  41. package/generated/prisma-mongodb/wasm.js +43 -4
  42. package/generated/prisma-postgresql/edge.js +43 -4
  43. package/generated/prisma-postgresql/index-browser.js +40 -1
  44. package/generated/prisma-postgresql/index.d.ts +4961 -28
  45. package/generated/prisma-postgresql/index.js +43 -4
  46. package/generated/prisma-postgresql/package.json +1 -1
  47. package/generated/prisma-postgresql/schema.prisma +87 -0
  48. package/generated/prisma-postgresql/wasm.js +43 -4
  49. package/handlers/WEBHOOKS.md +0 -29
  50. package/handlers/app-definition-loader.js +121 -19
  51. package/handlers/app-handler-helpers.js +25 -3
  52. package/handlers/backend-utils.js +31 -25
  53. package/handlers/middleware/admin-auth.js +14 -1
  54. package/handlers/routers/auth.js +46 -10
  55. package/handlers/routers/health.js +13 -4
  56. package/handlers/routers/integration-defined-routers.js +144 -92
  57. package/handlers/routers/integration-webhook-routers.js +102 -64
  58. package/handlers/workers/integration-defined-workers.js +5 -2
  59. package/index.js +65 -3
  60. package/infrastructure/scheduler/index.js +19 -6
  61. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  62. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  63. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  64. package/integrations/index.js +1 -0
  65. package/integrations/integration-base.js +0 -29
  66. package/integrations/integration-router.js +131 -96
  67. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  68. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  71. package/integrations/repositories/integration-mapping-repository.js +0 -29
  72. package/integrations/repositories/integration-not-found.js +14 -0
  73. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  74. package/integrations/repositories/integration-repository-interface.js +17 -31
  75. package/integrations/repositories/integration-repository-mongo.js +60 -129
  76. package/integrations/repositories/integration-repository-postgres.js +62 -134
  77. package/integrations/repositories/process-repository-mongo.js +1 -1
  78. package/integrations/schedule-config.js +80 -0
  79. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  80. package/integrations/use-cases/create-integration.js +72 -75
  81. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  82. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  83. package/integrations/use-cases/index.js +7 -1
  84. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  85. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  86. package/integrations/utils/map-integration-dto.js +43 -14
  87. package/management-api/create-management-api-router.js +49 -0
  88. package/management-api/error-response.js +116 -0
  89. package/management-api/management-api-config.js +151 -0
  90. package/management-api/meta-router.js +76 -0
  91. package/management-api/openapi/build-openapi.js +144 -0
  92. package/management-api/openapi/components.js +408 -0
  93. package/management-api/openapi/v1-operations.js +83 -0
  94. package/management-api/openapi/v2-operations.js +264 -0
  95. package/management-api/route-registry.js +411 -0
  96. package/management-api/test-support/http-client.js +39 -0
  97. package/management-api/use-cases/get-management-api-meta.js +57 -0
  98. package/management-api/v1-deprecation.js +120 -0
  99. package/management-api/v2/create-v2-router.js +85 -0
  100. package/management-api/v2/dependencies.js +281 -0
  101. package/management-api/v2/dto.js +143 -0
  102. package/management-api/v2/handlers/authorization.js +94 -0
  103. package/management-api/v2/handlers/credentials.js +33 -0
  104. package/management-api/v2/handlers/entities.js +77 -0
  105. package/management-api/v2/handlers/integrations.js +128 -0
  106. package/management-api/v2/handlers/proxy.js +34 -0
  107. package/management-api/v2/request.js +85 -0
  108. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  109. package/modules/domain/authorization-session.js +144 -0
  110. package/modules/entity-scope.js +15 -0
  111. package/modules/entity-type-registry.js +311 -0
  112. package/modules/index.js +14 -0
  113. package/modules/module-factory.js +12 -0
  114. package/modules/module.js +53 -2
  115. package/modules/proxy/proxy-errors.js +20 -0
  116. package/modules/proxy/proxy-path.js +179 -0
  117. package/modules/proxy/proxy-policy.js +248 -0
  118. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  119. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  120. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  121. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  122. package/modules/repositories/entity-not-found.js +18 -0
  123. package/modules/repositories/module-repository-documentdb.js +51 -6
  124. package/modules/repositories/module-repository-interface.js +19 -2
  125. package/modules/repositories/module-repository-mongo.js +67 -74
  126. package/modules/repositories/module-repository-postgres.js +66 -74
  127. package/modules/repositories/module-repository.js +2 -1
  128. package/modules/requester/proxy-request.js +244 -0
  129. package/modules/requester/requester.js +27 -2
  130. package/modules/scope-config.js +192 -0
  131. package/modules/scope-policy.js +112 -0
  132. package/modules/scope-resolution.js +288 -0
  133. package/modules/scope-visibility.js +247 -0
  134. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  135. package/modules/tests/doubles/ownership-matching.js +18 -0
  136. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  137. package/modules/use-cases/create-entity-from-credential.js +113 -0
  138. package/modules/use-cases/entity-operations-for-user.js +162 -0
  139. package/modules/use-cases/entity-types.js +139 -0
  140. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  141. package/modules/use-cases/get-authorization-requirements.js +100 -0
  142. package/modules/use-cases/get-entities-for-user.js +26 -13
  143. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  144. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  145. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  146. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  147. package/modules/use-cases/get-module.js +10 -4
  148. package/modules/use-cases/manage-access.js +21 -0
  149. package/modules/use-cases/multi-step-authorization.js +329 -0
  150. package/modules/use-cases/process-authorization-callback.js +53 -19
  151. package/modules/use-cases/refresh-entity-options.js +10 -4
  152. package/modules/use-cases/test-module-auth.js +10 -4
  153. package/modules/utils/map-module-dto.js +3 -5
  154. package/package.json +6 -5
  155. package/prisma-mongodb/schema.prisma +92 -0
  156. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  157. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  158. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  159. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  160. package/prisma-postgresql/schema.prisma +87 -0
  161. package/providers/index.js +23 -0
  162. package/providers/resolve-provider.js +93 -0
  163. package/providers/runtime-detection.js +124 -0
  164. package/queues/index.js +25 -0
  165. package/queues/providers/index.js +11 -0
  166. package/queues/providers/netlify-background-provider.js +150 -0
  167. package/queues/providers/qstash-queue-provider.js +219 -0
  168. package/queues/providers/sqs-queue-provider.js +70 -0
  169. package/queues/queue-client-interface.js +55 -0
  170. package/queues/queue-message-auth.js +193 -0
  171. package/queues/queue-provider-factory.js +101 -0
  172. package/queues/queue-provider.js +65 -0
  173. package/queues/sqs-queue-client.js +81 -0
  174. package/types/index.d.ts +2 -0
  175. package/types/modules/index.d.ts +197 -0
  176. package/types/user/index.d.ts +134 -0
  177. package/user/adopter-jwt-config.js +177 -0
  178. package/user/adopter-jwt-errors.js +59 -0
  179. package/user/adopter-token-verifier.js +122 -0
  180. package/user/app-user-helpers.js +97 -0
  181. package/user/create-user-authentication.js +64 -0
  182. package/user/membership.js +51 -0
  183. package/user/repositories/organization-membership-record.js +19 -0
  184. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  185. package/user/repositories/organization-membership-repository-factory.js +42 -0
  186. package/user/repositories/organization-membership-repository-interface.js +57 -0
  187. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  188. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  189. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  190. package/user/repositories/user-repository-documentdb.js +27 -2
  191. package/user/repositories/user-repository-interface.js +18 -0
  192. package/user/repositories/user-repository-mongo.js +24 -0
  193. package/user/repositories/user-repository-postgres.js +18 -0
  194. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  195. package/user/tests/doubles/test-user-repository.js +1 -1
  196. package/user/use-cases/authenticate-user.js +7 -1
  197. package/user/use-cases/ensure-organization-membership.js +92 -0
  198. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  199. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  200. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  201. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  202. package/user/user.js +64 -1
  203. package/errors/mapping-already-exists-error.js +0 -14
  204. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  205. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  206. package/integrations/utils/assert-entity-ids-input.js +0 -42
  207. package/modules/utils/to-credential-id.js +0 -14
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The error `findIntegrationById` throws for an id with no row, and the one
3
+ * predicate that recognises it. The adapters build the error here and the
4
+ * use cases match it here, so the message is written once and a missing
5
+ * integration can be told from any other failure without widening a regex.
6
+ */
7
+ const MESSAGE = /^Integration with id .+ not found$/;
8
+
9
+ const integrationNotFoundError = (id) =>
10
+ new Error(`Integration with id ${id} not found`);
11
+
12
+ const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
13
+
14
+ module.exports = { integrationNotFoundError, isIntegrationNotFound };
@@ -14,6 +14,7 @@ const {
14
14
  IntegrationRepositoryInterface,
15
15
  } = require('./integration-repository-interface');
16
16
  const { validateConfigPatch } = require('./config-patch-shared');
17
+ const { integrationNotFoundError } = require('./integration-not-found');
17
18
 
18
19
  class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
19
20
  constructor() {
@@ -28,6 +29,28 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
28
29
  return records.map((doc) => this._mapIntegration(doc));
29
30
  }
30
31
 
32
+ /**
33
+ * Find integrations matching any of the owner/organization pairs, in one
34
+ * query. `organizationId: null` matches rows with no organization (a raw
35
+ * MongoDB `null` also matches a missing field). The caller (a use case)
36
+ * decides which pairs; this only runs them.
37
+ *
38
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
39
+ * @returns {Promise<Array>}
40
+ */
41
+ async findIntegrationsByOwnership(clauses) {
42
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
43
+ // Drained: a list can hold more than the first cursor batch (~101).
44
+ const records = await findManyDrained(this.prisma, 'Integration', {
45
+ $or: clauses.map(({ userId, organizationId }) => ({
46
+ userId: toObjectId(userId),
47
+ organizationId:
48
+ organizationId === null ? null : toObjectId(organizationId),
49
+ })),
50
+ });
51
+ return records.map((doc) => this._mapIntegration(doc));
52
+ }
53
+
31
54
  async findIntegrations({ type, status } = {}) {
32
55
  const filter = {};
33
56
  if (type) {
@@ -68,11 +91,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
68
91
  async findIntegrationById(id) {
69
92
  const objectId = toObjectId(id);
70
93
  if (!objectId) {
71
- throw new Error(`Integration with id ${id} not found`);
94
+ throw integrationNotFoundError(id);
72
95
  }
73
96
  const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
74
97
  if (!doc) {
75
- throw new Error(`Integration with id ${id} not found`);
98
+ throw integrationNotFoundError(id);
76
99
  }
77
100
  return this._mapIntegration(doc);
78
101
  }
@@ -132,10 +155,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
132
155
  return true;
133
156
  }
134
157
 
135
- async createIntegration(entities, userId, config) {
158
+ /**
159
+ * @param {Array<string>} entities - entity ids
160
+ * @param {string} userId - owner (the person, or the organization's user)
161
+ * @param {Object} config - Integration configuration
162
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
163
+ * integration is created in (ADR-056 §3.6)
164
+ */
165
+ async createIntegration(entities, userId, config, tenancy = {}) {
136
166
  const now = new Date();
137
167
  const document = {
138
168
  userId: toObjectId(userId) || null,
169
+ organizationId: toObjectId(tenancy.organizationId) || null,
139
170
  config,
140
171
  version: '0.0.0',
141
172
  status: 'IN_CREATION',
@@ -164,38 +195,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
164
195
  return this._mapIntegration(created);
165
196
  }
166
197
 
167
- async addEntitiesToIntegration(integrationId, entityIds) {
168
- const objectId = toObjectId(integrationId);
169
- if (!objectId) return null;
170
- const result = await updateOne(
171
- this.prisma,
172
- 'Integration',
173
- { _id: objectId },
174
- {
175
- $addToSet: { entityIds: { $each: toObjectIdArray(entityIds) } },
176
- $set: { updatedAt: { $date: new Date().toISOString() } },
177
- }
178
- );
179
- if (!result?.n) return null;
180
- return this.findIntegrationById(integrationId);
181
- }
182
-
183
- async removeEntitiesFromIntegration(integrationId, entityIds) {
184
- const objectId = toObjectId(integrationId);
185
- if (!objectId) return null;
186
- const result = await updateOne(
187
- this.prisma,
188
- 'Integration',
189
- { _id: objectId },
190
- {
191
- $pullAll: { entityIds: toObjectIdArray(entityIds) },
192
- $set: { updatedAt: { $date: new Date().toISOString() } },
193
- }
194
- );
195
- if (!result?.n) return null;
196
- return this.findIntegrationById(integrationId);
197
- }
198
-
199
198
  async findIntegrationByUserId(userId) {
200
199
  const objectId = toObjectId(userId);
201
200
  if (!objectId) return null;
@@ -330,6 +329,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
330
329
  id: fromObjectId(doc?._id),
331
330
  entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
332
331
  userId: fromObjectId(doc?.userId),
332
+ organizationId: fromObjectId(doc?.organizationId) ?? null,
333
333
  config: doc?.config ?? null,
334
334
  version: doc?.version ?? null,
335
335
  status: doc?.status ?? null,
@@ -10,6 +10,20 @@
10
10
  * @abstract
11
11
  */
12
12
  class IntegrationRepositoryInterface {
13
+ /**
14
+ * Find integrations matching any of the owner/organization pairs, in one
15
+ * query (ADR-056 §3.6). `organizationId: null` matches rows with no
16
+ * organization. Which pairs is decided by the caller
17
+ * (modules/entity-scope.js), not here.
18
+ *
19
+ * @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
20
+ * @returns {Promise<Array>} Array of integration objects
21
+ * @abstract
22
+ */
23
+ async findIntegrationsByOwnership(clauses) {
24
+ throw new Error('Method findIntegrationsByOwnership must be implemented by subclass');
25
+ }
26
+
13
27
  /**
14
28
  * Find all integrations for a user
15
29
  *
@@ -123,10 +137,12 @@ class IntegrationRepositoryInterface {
123
137
  * @param {Array<string|number>} entities - Array of entity IDs
124
138
  * @param {string|number} userId - User ID
125
139
  * @param {Object} config - Integration configuration
140
+ * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
141
+ * integration is created in (ADR-056 §3.6)
126
142
  * @returns {Promise<Object>} Created integration object
127
143
  * @abstract
128
144
  */
129
- async createIntegration(entities, userId, config) {
145
+ async createIntegration(entities, userId, config, tenancy) {
130
146
  throw new Error('Method createIntegration must be implemented by subclass');
131
147
  }
132
148
 
@@ -184,36 +200,6 @@ class IntegrationRepositoryInterface {
184
200
  'Method findIntegrationsByEntityId must be implemented by subclass'
185
201
  );
186
202
  }
187
-
188
- /**
189
- * Attach entities to an existing integration. Entities that are already
190
- * attached are left as they are.
191
- *
192
- * @param {string|number} integrationId - Integration ID
193
- * @param {Array<string|number>} entityIds - Entity IDs to attach
194
- * @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
195
- * @abstract
196
- */
197
- async addEntitiesToIntegration(integrationId, entityIds) {
198
- throw new Error(
199
- 'Method addEntitiesToIntegration must be implemented by subclass'
200
- );
201
- }
202
-
203
- /**
204
- * Detach entities from an integration. Entities that are not attached
205
- * are ignored; the entities themselves are not deleted.
206
- *
207
- * @param {string|number} integrationId - Integration ID
208
- * @param {Array<string|number>} entityIds - Entity IDs to detach
209
- * @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
210
- * @abstract
211
- */
212
- async removeEntitiesFromIntegration(integrationId, entityIds) {
213
- throw new Error(
214
- 'Method removeEntitiesFromIntegration must be implemented by subclass'
215
- );
216
- }
217
203
  }
218
204
 
219
205
  module.exports = { IntegrationRepositoryInterface };
@@ -1,8 +1,10 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { organizationWhere } = require('../../database/organization-where');
2
3
  const {
3
4
  IntegrationRepositoryInterface,
4
5
  } = require('./integration-repository-interface');
5
6
  const { validateConfigPatch } = require('./config-patch-shared');
7
+ const { integrationNotFoundError } = require('./integration-not-found');
6
8
 
7
9
  /**
8
10
  * MongoDB Integration Repository Adapter
@@ -25,6 +27,25 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
25
27
  this.prisma = prisma;
26
28
  }
27
29
 
30
+ /**
31
+ * The one place a integration integration row becomes the record the
32
+ * application sees (ids of its entities, its organization).
33
+ * @private
34
+ */
35
+ _mapIntegration(integration) {
36
+ return {
37
+ id: integration.id,
38
+ entitiesIds: integration.entities.map((e) => e.id),
39
+ userId: integration.userId,
40
+ organizationId: integration.organizationId ?? null,
41
+ config: integration.config,
42
+ version: integration.version,
43
+ status: integration.status,
44
+ messages: integration.messages,
45
+ createdAt: integration.createdAt,
46
+ };
47
+ }
48
+
28
49
  /**
29
50
  * Find all integrations for a user
30
51
  * Replaces: IntegrationModel.find({ user: userId }).populate('entities')
@@ -41,16 +62,32 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
41
62
  });
42
63
 
43
64
  // Map to domain objects (maintains same API)
44
- return integrations.map((integration) => ({
45
- id: integration.id,
46
- entitiesIds: integration.entities.map((e) => e.id),
47
- userId: integration.userId,
48
- config: integration.config,
49
- version: integration.version,
50
- status: integration.status,
51
- messages: integration.messages,
52
- createdAt: integration.createdAt,
53
- }));
65
+ return integrations.map((integration) => (this._mapIntegration(integration)));
66
+ }
67
+
68
+ /**
69
+ * Find integrations matching any of the owner/organization pairs, in one
70
+ * query. `organizationId: null` matches rows with no organization. The
71
+ * caller (a use case) decides which pairs; this only runs them.
72
+ *
73
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
74
+ * @returns {Promise<Array>} Array of integration objects
75
+ */
76
+ async findIntegrationsByOwnership(clauses) {
77
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
78
+ const integrations = await this.prisma.integration.findMany({
79
+ where: {
80
+ OR: clauses.map(({ userId, organizationId }) => ({
81
+ userId,
82
+ ...organizationWhere(organizationId),
83
+ })),
84
+ },
85
+ include: {
86
+ entities: true,
87
+ },
88
+ });
89
+
90
+ return integrations.map((integration) => (this._mapIntegration(integration)));
54
91
  }
55
92
 
56
93
  /**
@@ -77,16 +114,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
77
114
  },
78
115
  });
79
116
 
80
- return integrations.map((integration) => ({
81
- id: integration.id,
82
- entitiesIds: integration.entities.map((e) => e.id),
83
- userId: integration.userId,
84
- config: integration.config,
85
- version: integration.version,
86
- status: integration.status,
87
- messages: integration.messages,
88
- createdAt: integration.createdAt,
89
- }));
117
+ return integrations.map((integration) => (this._mapIntegration(integration)));
90
118
  }
91
119
 
92
120
  /**
@@ -165,15 +193,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
165
193
  throw new Error(`Integration with name ${name} not found`);
166
194
  }
167
195
 
168
- return {
169
- id: integration.id,
170
- entitiesIds: integration.entities.map((e) => e.id),
171
- userId: integration.userId,
172
- config: integration.config,
173
- version: integration.version,
174
- status: integration.status,
175
- messages: integration.messages,
176
- };
196
+ return this._mapIntegration(integration);
177
197
  }
178
198
 
179
199
  /**
@@ -192,18 +212,10 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
192
212
  });
193
213
 
194
214
  if (!integration) {
195
- throw new Error(`Integration with id ${id} not found`);
215
+ throw integrationNotFoundError(id);
196
216
  }
197
217
 
198
- return {
199
- id: integration.id,
200
- entitiesIds: integration.entities.map((e) => e.id),
201
- userId: integration.userId,
202
- config: integration.config,
203
- version: integration.version,
204
- status: integration.status,
205
- messages: integration.messages,
206
- };
218
+ return this._mapIntegration(integration);
207
219
  }
208
220
 
209
221
  /**
@@ -290,15 +302,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
290
302
  },
291
303
  });
292
304
 
293
- return integrations.map((integration) => ({
294
- id: integration.id,
295
- entitiesIds: integration.entities.map((e) => e.id),
296
- userId: integration.userId,
297
- config: integration.config,
298
- version: integration.version,
299
- status: integration.status,
300
- messages: integration.messages,
301
- }));
305
+ return integrations.map((integration) => (this._mapIntegration(integration)));
302
306
  }
303
307
 
304
308
  /**
@@ -310,13 +314,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
310
314
  * @param {Array<string>} entities - Array of entity IDs (MongoDB ObjectIds)
311
315
  * @param {string} userId - User ID (MongoDB ObjectId)
312
316
  * @param {Object} config - Integration configuration
317
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
318
+ * integration is created in (ADR-056 §3.6)
313
319
  * @returns {Promise<Object>} Created integration object
314
320
  */
315
- async createIntegration(entities, userId, config) {
321
+ async createIntegration(entities, userId, config, tenancy = {}) {
316
322
  const data = {
317
323
  config,
318
324
  version: '0.0.0',
319
325
  userId: userId,
326
+ organizationId: tenancy.organizationId ?? null,
320
327
  entityIds: entities,
321
328
  };
322
329
 
@@ -327,67 +334,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
327
334
  },
328
335
  });
329
336
 
330
- return {
331
- id: integration.id,
332
- entitiesIds: integration.entities.map((e) => e.id),
333
- userId: integration.userId,
334
- config: integration.config,
335
- version: integration.version,
336
- status: integration.status,
337
- messages: integration.messages,
338
- };
339
- }
340
-
341
- /**
342
- * Attach entities to an existing integration. Entities already attached
343
- * are left as they are.
344
- *
345
- * @param {string} integrationId - Integration ID
346
- * @param {Array<string>} entityIds - Entity IDs to attach
347
- * @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
348
- */
349
- async addEntitiesToIntegration(integrationId, entityIds) {
350
-
351
- try {
352
- await this.prisma.integration.update({
353
- where: { id: integrationId },
354
- data: {
355
- entities: {
356
- connect: entityIds.map((id) => ({ id })),
357
- },
358
- },
359
- });
360
- } catch (error) {
361
- if (error.code === 'P2025') return null;
362
- throw error;
363
- }
364
- return this.findIntegrationById(integrationId);
365
- }
366
-
367
- /**
368
- * Detach entities from an integration. Entities that are not attached
369
- * are ignored. The entities themselves are not deleted.
370
- *
371
- * @param {string} integrationId - Integration ID
372
- * @param {Array<string>} entityIds - Entity IDs to detach
373
- * @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
374
- */
375
- async removeEntitiesFromIntegration(integrationId, entityIds) {
376
-
377
- try {
378
- await this.prisma.integration.update({
379
- where: { id: integrationId },
380
- data: {
381
- entities: {
382
- disconnect: entityIds.map((id) => ({ id })),
383
- },
384
- },
385
- });
386
- } catch (error) {
387
- if (error.code === 'P2025') return null;
388
- throw error;
389
- }
390
- return this.findIntegrationById(integrationId);
337
+ return this._mapIntegration(integration);
391
338
  }
392
339
 
393
340
  /**
@@ -409,15 +356,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
409
356
  return null;
410
357
  }
411
358
 
412
- return {
413
- id: integration.id,
414
- entitiesIds: integration.entities.map((e) => e.id),
415
- userId: integration.userId,
416
- config: integration.config,
417
- version: integration.version,
418
- status: integration.status,
419
- messages: integration.messages,
420
- };
359
+ return this._mapIntegration(integration);
421
360
  }
422
361
 
423
362
  /**
@@ -441,15 +380,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
441
380
  },
442
381
  });
443
382
 
444
- return {
445
- id: integration.id,
446
- entitiesIds: integration.entities.map((e) => e.id),
447
- userId: integration.userId,
448
- config: integration.config,
449
- version: integration.version,
450
- status: integration.status,
451
- messages: integration.messages,
452
- };
383
+ return this._mapIntegration(integration);
453
384
  }
454
385
 
455
386
  /**
@@ -471,7 +402,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
471
402
  for (const [key, value] of Object.entries(patch)) {
472
403
  $set[`config.${key}`] = value;
473
404
  }
474
- $set.updatedAt = { $date: new Date().toISOString() };
405
+ $set.updatedAt = new Date();
475
406
 
476
407
  const result = await this.prisma.$runCommandRaw({
477
408
  findAndModify: 'Integration',