@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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 (217) hide show
  1. package/CHANGELOG.md +0 -16
  2. package/CLAUDE.md +41 -29
  3. package/README.md +0 -105
  4. package/application/commands/README.md +17 -11
  5. package/application/commands/entity-commands.js +18 -60
  6. package/application/commands/integration-commands.js +73 -20
  7. package/application/commands/scheduler-commands.js +7 -11
  8. package/application/commands/user-commands.js +5 -17
  9. package/application/index.js +2 -6
  10. package/core/Worker.js +7 -31
  11. package/core/create-handler.js +0 -9
  12. package/credential/repositories/credential-repository-documentdb.js +0 -34
  13. package/credential/repositories/credential-repository-interface.js +0 -15
  14. package/credential/repositories/credential-repository-mongo.js +0 -23
  15. package/credential/repositories/credential-repository-postgres.js +0 -28
  16. package/database/config.js +24 -48
  17. package/database/documentdb-encryption-service.js +22 -9
  18. package/database/documentdb-utils.js +0 -2
  19. package/database/encryption/README.md +17 -81
  20. package/database/encryption/documentdb-encryption-service.md +1 -78
  21. package/database/encryption/encryption-schema-registry.js +46 -123
  22. package/database/encryption/field-encryption-service.js +5 -69
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/prisma.js +38 -15
  25. package/database/use-cases/check-encryption-health-use-case.js +52 -43
  26. package/errors/index.js +4 -0
  27. package/errors/mapping-already-exists-error.js +14 -0
  28. package/generated/prisma-mongodb/edge.js +4 -43
  29. package/generated/prisma-mongodb/index-browser.js +1 -40
  30. package/generated/prisma-mongodb/index.d.ts +111 -4739
  31. package/generated/prisma-mongodb/index.js +4 -43
  32. package/generated/prisma-mongodb/package.json +1 -1
  33. package/generated/prisma-mongodb/schema.prisma +0 -92
  34. package/generated/prisma-mongodb/wasm.js +4 -43
  35. package/generated/prisma-postgresql/edge.js +4 -43
  36. package/generated/prisma-postgresql/index-browser.js +1 -40
  37. package/generated/prisma-postgresql/index.d.ts +33 -4966
  38. package/generated/prisma-postgresql/index.js +4 -43
  39. package/generated/prisma-postgresql/package.json +1 -1
  40. package/generated/prisma-postgresql/schema.prisma +0 -87
  41. package/generated/prisma-postgresql/wasm.js +4 -43
  42. package/handlers/WEBHOOKS.md +29 -0
  43. package/handlers/app-definition-loader.js +19 -121
  44. package/handlers/app-handler-helpers.js +3 -25
  45. package/handlers/backend-utils.js +25 -31
  46. package/handlers/middleware/admin-auth.js +1 -14
  47. package/handlers/routers/auth.js +10 -46
  48. package/handlers/routers/health.js +4 -13
  49. package/handlers/routers/integration-defined-routers.js +92 -144
  50. package/handlers/routers/integration-webhook-routers.js +64 -102
  51. package/handlers/workers/integration-defined-workers.js +2 -5
  52. package/index.js +3 -65
  53. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
  54. package/infrastructure/scheduler/index.js +6 -19
  55. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  56. package/integrations/index.js +0 -1
  57. package/integrations/integration-base.js +29 -0
  58. package/integrations/integration-router.js +96 -131
  59. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  60. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  61. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  62. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  63. package/integrations/repositories/integration-mapping-repository.js +29 -0
  64. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  65. package/integrations/repositories/integration-repository-interface.js +31 -17
  66. package/integrations/repositories/integration-repository-mongo.js +129 -60
  67. package/integrations/repositories/integration-repository-postgres.js +134 -62
  68. package/integrations/repositories/process-repository-mongo.js +1 -1
  69. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  70. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  71. package/integrations/use-cases/create-integration.js +75 -72
  72. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  73. package/integrations/use-cases/index.js +1 -7
  74. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  75. package/integrations/utils/assert-entity-ids-input.js +42 -0
  76. package/integrations/utils/map-integration-dto.js +14 -43
  77. package/jest-setup.js +13 -2
  78. package/jest-teardown.js +5 -2
  79. package/jest.config.js +4 -0
  80. package/jest.db.config.js +23 -0
  81. package/modules/index.js +0 -14
  82. package/modules/module-factory.js +0 -12
  83. package/modules/module.js +2 -53
  84. package/modules/repositories/module-repository-documentdb.js +6 -51
  85. package/modules/repositories/module-repository-interface.js +2 -19
  86. package/modules/repositories/module-repository-mongo.js +74 -67
  87. package/modules/repositories/module-repository-postgres.js +74 -66
  88. package/modules/repositories/module-repository.js +1 -2
  89. package/modules/requester/requester.js +2 -27
  90. package/modules/use-cases/get-entities-for-user.js +13 -26
  91. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  92. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  93. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  94. package/modules/use-cases/get-module.js +4 -10
  95. package/modules/use-cases/process-authorization-callback.js +19 -53
  96. package/modules/use-cases/refresh-entity-options.js +4 -10
  97. package/modules/use-cases/test-module-auth.js +4 -10
  98. package/modules/utils/map-module-dto.js +5 -3
  99. package/modules/utils/to-credential-id.js +14 -0
  100. package/package.json +7 -6
  101. package/prisma-mongodb/schema.prisma +0 -92
  102. package/prisma-postgresql/schema.prisma +0 -87
  103. package/queues/index.js +0 -25
  104. package/queues/queuer-util.js +6 -5
  105. package/types/index.d.ts +0 -2
  106. package/user/repositories/user-repository-documentdb.js +2 -27
  107. package/user/repositories/user-repository-interface.js +0 -18
  108. package/user/repositories/user-repository-mongo.js +0 -24
  109. package/user/repositories/user-repository-postgres.js +0 -18
  110. package/user/tests/doubles/test-user-repository.js +1 -1
  111. package/user/use-cases/authenticate-user.js +1 -7
  112. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  113. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  115. package/user/user.js +1 -64
  116. package/credential/use-cases/credential-operations-for-user.js +0 -202
  117. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  118. package/database/encryption/encryption-config.js +0 -219
  119. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  120. package/database/organization-where.js +0 -18
  121. package/database/unique-violation.js +0 -25
  122. package/extensions/bootstrap-runner.js +0 -43
  123. package/extensions/extension-loader.js +0 -187
  124. package/extensions/index.js +0 -41
  125. package/extensions/initialize-app.js +0 -123
  126. package/extensions/route-mounter.js +0 -103
  127. package/extensions/schema-composer.js +0 -188
  128. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
  129. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  130. package/integrations/repositories/integration-not-found.js +0 -14
  131. package/integrations/schedule-config.js +0 -80
  132. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  133. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  134. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  135. package/management-api/create-management-api-router.js +0 -49
  136. package/management-api/error-response.js +0 -116
  137. package/management-api/management-api-config.js +0 -151
  138. package/management-api/meta-router.js +0 -76
  139. package/management-api/openapi/build-openapi.js +0 -144
  140. package/management-api/openapi/components.js +0 -408
  141. package/management-api/openapi/v1-operations.js +0 -83
  142. package/management-api/openapi/v2-operations.js +0 -264
  143. package/management-api/route-registry.js +0 -411
  144. package/management-api/test-support/http-client.js +0 -39
  145. package/management-api/use-cases/get-management-api-meta.js +0 -57
  146. package/management-api/v1-deprecation.js +0 -120
  147. package/management-api/v2/create-v2-router.js +0 -85
  148. package/management-api/v2/dependencies.js +0 -281
  149. package/management-api/v2/dto.js +0 -143
  150. package/management-api/v2/handlers/authorization.js +0 -94
  151. package/management-api/v2/handlers/credentials.js +0 -33
  152. package/management-api/v2/handlers/entities.js +0 -77
  153. package/management-api/v2/handlers/integrations.js +0 -128
  154. package/management-api/v2/handlers/proxy.js +0 -34
  155. package/management-api/v2/request.js +0 -85
  156. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  157. package/modules/domain/authorization-session.js +0 -144
  158. package/modules/entity-scope.js +0 -15
  159. package/modules/entity-type-registry.js +0 -311
  160. package/modules/proxy/proxy-errors.js +0 -20
  161. package/modules/proxy/proxy-path.js +0 -179
  162. package/modules/proxy/proxy-policy.js +0 -248
  163. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  164. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  165. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  166. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  167. package/modules/repositories/entity-not-found.js +0 -18
  168. package/modules/requester/proxy-request.js +0 -244
  169. package/modules/scope-config.js +0 -192
  170. package/modules/scope-policy.js +0 -112
  171. package/modules/scope-resolution.js +0 -288
  172. package/modules/scope-visibility.js +0 -247
  173. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  174. package/modules/tests/doubles/ownership-matching.js +0 -18
  175. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  176. package/modules/use-cases/create-entity-from-credential.js +0 -113
  177. package/modules/use-cases/entity-operations-for-user.js +0 -162
  178. package/modules/use-cases/entity-types.js +0 -139
  179. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  180. package/modules/use-cases/get-authorization-requirements.js +0 -100
  181. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  182. package/modules/use-cases/manage-access.js +0 -21
  183. package/modules/use-cases/multi-step-authorization.js +0 -329
  184. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  185. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  186. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  187. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  188. package/providers/index.js +0 -23
  189. package/providers/resolve-provider.js +0 -93
  190. package/providers/runtime-detection.js +0 -124
  191. package/queues/providers/index.js +0 -11
  192. package/queues/providers/netlify-background-provider.js +0 -187
  193. package/queues/providers/qstash-queue-provider.js +0 -219
  194. package/queues/providers/sqs-queue-provider.js +0 -70
  195. package/queues/queue-client-interface.js +0 -55
  196. package/queues/queue-message-auth.js +0 -193
  197. package/queues/queue-provider-factory.js +0 -101
  198. package/queues/queue-provider.js +0 -65
  199. package/queues/sqs-queue-client.js +0 -81
  200. package/types/modules/index.d.ts +0 -197
  201. package/types/user/index.d.ts +0 -134
  202. package/user/adopter-jwt-config.js +0 -177
  203. package/user/adopter-jwt-errors.js +0 -59
  204. package/user/adopter-token-verifier.js +0 -122
  205. package/user/app-user-helpers.js +0 -97
  206. package/user/create-user-authentication.js +0 -64
  207. package/user/membership.js +0 -51
  208. package/user/repositories/organization-membership-record.js +0 -19
  209. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  210. package/user/repositories/organization-membership-repository-factory.js +0 -42
  211. package/user/repositories/organization-membership-repository-interface.js +0 -57
  212. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  213. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  214. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  215. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  216. package/user/use-cases/ensure-organization-membership.js +0 -92
  217. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -14,7 +14,6 @@ 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');
18
17
 
19
18
  class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
20
19
  constructor() {
@@ -29,28 +28,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
29
28
  return records.map((doc) => this._mapIntegration(doc));
30
29
  }
31
30
 
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
-
54
31
  async findIntegrations({ type, status } = {}) {
55
32
  const filter = {};
56
33
  if (type) {
@@ -91,11 +68,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
91
68
  async findIntegrationById(id) {
92
69
  const objectId = toObjectId(id);
93
70
  if (!objectId) {
94
- throw integrationNotFoundError(id);
71
+ throw new Error(`Integration with id ${id} not found`);
95
72
  }
96
73
  const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
97
74
  if (!doc) {
98
- throw integrationNotFoundError(id);
75
+ throw new Error(`Integration with id ${id} not found`);
99
76
  }
100
77
  return this._mapIntegration(doc);
101
78
  }
@@ -155,18 +132,10 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
155
132
  return true;
156
133
  }
157
134
 
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 = {}) {
135
+ async createIntegration(entities, userId, config) {
166
136
  const now = new Date();
167
137
  const document = {
168
138
  userId: toObjectId(userId) || null,
169
- organizationId: toObjectId(tenancy.organizationId) || null,
170
139
  config,
171
140
  version: '0.0.0',
172
141
  status: 'IN_CREATION',
@@ -195,6 +164,38 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
195
164
  return this._mapIntegration(created);
196
165
  }
197
166
 
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
+
198
199
  async findIntegrationByUserId(userId) {
199
200
  const objectId = toObjectId(userId);
200
201
  if (!objectId) return null;
@@ -329,7 +330,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
329
330
  id: fromObjectId(doc?._id),
330
331
  entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
331
332
  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,20 +10,6 @@
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
-
27
13
  /**
28
14
  * Find all integrations for a user
29
15
  *
@@ -137,12 +123,10 @@ class IntegrationRepositoryInterface {
137
123
  * @param {Array<string|number>} entities - Array of entity IDs
138
124
  * @param {string|number} userId - User ID
139
125
  * @param {Object} config - Integration configuration
140
- * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
141
- * integration is created in (ADR-056 §3.6)
142
126
  * @returns {Promise<Object>} Created integration object
143
127
  * @abstract
144
128
  */
145
- async createIntegration(entities, userId, config, tenancy) {
129
+ async createIntegration(entities, userId, config) {
146
130
  throw new Error('Method createIntegration must be implemented by subclass');
147
131
  }
148
132
 
@@ -200,6 +184,36 @@ class IntegrationRepositoryInterface {
200
184
  'Method findIntegrationsByEntityId must be implemented by subclass'
201
185
  );
202
186
  }
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
+ }
203
217
  }
204
218
 
205
219
  module.exports = { IntegrationRepositoryInterface };
@@ -1,10 +1,8 @@
1
1
  const { prisma } = require('../../database/prisma');
2
- const { organizationWhere } = require('../../database/organization-where');
3
2
  const {
4
3
  IntegrationRepositoryInterface,
5
4
  } = require('./integration-repository-interface');
6
5
  const { validateConfigPatch } = require('./config-patch-shared');
7
- const { integrationNotFoundError } = require('./integration-not-found');
8
6
 
9
7
  /**
10
8
  * MongoDB Integration Repository Adapter
@@ -27,25 +25,6 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
27
25
  this.prisma = prisma;
28
26
  }
29
27
 
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
-
49
28
  /**
50
29
  * Find all integrations for a user
51
30
  * Replaces: IntegrationModel.find({ user: userId }).populate('entities')
@@ -62,32 +41,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
62
41
  });
63
42
 
64
43
  // Map to domain objects (maintains same API)
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)));
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
+ }));
91
54
  }
92
55
 
93
56
  /**
@@ -114,7 +77,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
114
77
  },
115
78
  });
116
79
 
117
- return integrations.map((integration) => (this._mapIntegration(integration)));
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
+ }));
118
90
  }
119
91
 
120
92
  /**
@@ -193,7 +165,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
193
165
  throw new Error(`Integration with name ${name} not found`);
194
166
  }
195
167
 
196
- return this._mapIntegration(integration);
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
+ };
197
177
  }
198
178
 
199
179
  /**
@@ -212,10 +192,18 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
212
192
  });
213
193
 
214
194
  if (!integration) {
215
- throw integrationNotFoundError(id);
195
+ throw new Error(`Integration with id ${id} not found`);
216
196
  }
217
197
 
218
- return this._mapIntegration(integration);
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
+ };
219
207
  }
220
208
 
221
209
  /**
@@ -302,7 +290,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
302
290
  },
303
291
  });
304
292
 
305
- return integrations.map((integration) => (this._mapIntegration(integration)));
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
+ }));
306
302
  }
307
303
 
308
304
  /**
@@ -314,16 +310,13 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
314
310
  * @param {Array<string>} entities - Array of entity IDs (MongoDB ObjectIds)
315
311
  * @param {string} userId - User ID (MongoDB ObjectId)
316
312
  * @param {Object} config - Integration configuration
317
- * @param {{organizationId?: (string|null)}} [tenancy] - the organization the
318
- * integration is created in (ADR-056 §3.6)
319
313
  * @returns {Promise<Object>} Created integration object
320
314
  */
321
- async createIntegration(entities, userId, config, tenancy = {}) {
315
+ async createIntegration(entities, userId, config) {
322
316
  const data = {
323
317
  config,
324
318
  version: '0.0.0',
325
319
  userId: userId,
326
- organizationId: tenancy.organizationId ?? null,
327
320
  entityIds: entities,
328
321
  };
329
322
 
@@ -334,7 +327,67 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
334
327
  },
335
328
  });
336
329
 
337
- return this._mapIntegration(integration);
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);
338
391
  }
339
392
 
340
393
  /**
@@ -356,7 +409,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
356
409
  return null;
357
410
  }
358
411
 
359
- return this._mapIntegration(integration);
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
+ };
360
421
  }
361
422
 
362
423
  /**
@@ -380,7 +441,15 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
380
441
  },
381
442
  });
382
443
 
383
- return this._mapIntegration(integration);
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
+ };
384
453
  }
385
454
 
386
455
  /**
@@ -402,7 +471,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
402
471
  for (const [key, value] of Object.entries(patch)) {
403
472
  $set[`config.${key}`] = value;
404
473
  }
405
- $set.updatedAt = new Date();
474
+ $set.updatedAt = { $date: new Date().toISOString() };
406
475
 
407
476
  const result = await this.prisma.$runCommandRaw({
408
477
  findAndModify: 'Integration',