@friggframework/core 2.0.0--canary.681.2df9ef0.0 → 2.0.0--canary.682.cbca9e2.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 (77) hide show
  1. package/README.md +36 -0
  2. package/application/commands/integration-commands.js +20 -10
  3. package/application/index.js +6 -2
  4. package/database/encryption/encryption-schema-registry.js +7 -0
  5. package/database/organization-where.js +18 -0
  6. package/generated/prisma-mongodb/edge.js +5 -3
  7. package/generated/prisma-mongodb/index-browser.js +2 -0
  8. package/generated/prisma-mongodb/index.d.ts +115 -2
  9. package/generated/prisma-mongodb/index.js +5 -3
  10. package/generated/prisma-mongodb/package.json +1 -1
  11. package/generated/prisma-mongodb/schema.prisma +15 -0
  12. package/generated/prisma-mongodb/wasm.js +5 -3
  13. package/generated/prisma-postgresql/edge.js +5 -3
  14. package/generated/prisma-postgresql/index-browser.js +2 -0
  15. package/generated/prisma-postgresql/index.d.ts +131 -2
  16. package/generated/prisma-postgresql/index.js +5 -3
  17. package/generated/prisma-postgresql/package.json +1 -1
  18. package/generated/prisma-postgresql/schema.prisma +15 -0
  19. package/generated/prisma-postgresql/wasm.js +5 -3
  20. package/handlers/app-definition-loader.js +25 -8
  21. package/handlers/backend-utils.js +31 -25
  22. package/handlers/routers/health.js +1 -4
  23. package/handlers/workers/integration-defined-workers.js +5 -2
  24. package/index.js +10 -2
  25. package/integrations/index.js +0 -4
  26. package/integrations/integration-router.js +99 -52
  27. package/integrations/repositories/integration-not-found.js +14 -0
  28. package/integrations/repositories/integration-repository-documentdb.js +34 -3
  29. package/integrations/repositories/integration-repository-interface.js +17 -1
  30. package/integrations/repositories/integration-repository-mongo.js +59 -76
  31. package/integrations/repositories/integration-repository-postgres.js +62 -76
  32. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  33. package/integrations/use-cases/create-integration.js +55 -20
  34. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  35. package/integrations/use-cases/index.js +5 -1
  36. package/integrations/use-cases/integration-operations-for-user.js +40 -19
  37. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  38. package/integrations/utils/map-integration-dto.js +1 -15
  39. package/management-api/management-api-config.js +3 -0
  40. package/management-api/openapi/components.js +46 -1
  41. package/management-api/openapi/v2-operations.js +14 -1
  42. package/management-api/v2/dependencies.js +56 -14
  43. package/management-api/v2/dto.js +17 -0
  44. package/management-api/v2/handlers/authorization.js +1 -0
  45. package/management-api/v2/handlers/entities.js +10 -7
  46. package/management-api/v2/handlers/integrations.js +6 -3
  47. package/management-api/v2/request.js +32 -1
  48. package/modules/entity-scope.js +15 -0
  49. package/modules/entity-type-registry.js +311 -0
  50. package/modules/index.js +14 -0
  51. package/modules/repositories/module-repository-documentdb.js +41 -1
  52. package/modules/repositories/module-repository-interface.js +19 -2
  53. package/modules/repositories/module-repository-mongo.js +64 -73
  54. package/modules/repositories/module-repository-postgres.js +63 -73
  55. package/modules/scope-config.js +155 -0
  56. package/modules/scope-policy.js +80 -0
  57. package/modules/scope-resolution.js +277 -0
  58. package/modules/scope-visibility.js +165 -0
  59. package/modules/tests/doubles/ownership-matching.js +18 -0
  60. package/modules/use-cases/authorize-entity-for-user.js +59 -0
  61. package/modules/use-cases/entity-operations-for-user.js +42 -10
  62. package/modules/use-cases/entity-types.js +62 -14
  63. package/modules/use-cases/execute-entity-proxy-request.js +15 -0
  64. package/modules/use-cases/get-entities-for-user.js +16 -5
  65. package/modules/use-cases/get-entity-module-for-user.js +23 -3
  66. package/modules/use-cases/get-entity-options-by-id.js +5 -2
  67. package/modules/use-cases/get-module.js +5 -2
  68. package/modules/use-cases/multi-step-authorization.js +36 -12
  69. package/modules/use-cases/process-authorization-callback.js +52 -18
  70. package/modules/use-cases/refresh-entity-options.js +5 -2
  71. package/modules/use-cases/test-module-auth.js +5 -2
  72. package/package.json +5 -5
  73. package/prisma-mongodb/schema.prisma +15 -0
  74. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  75. package/prisma-postgresql/schema.prisma +15 -0
  76. package/types/index.d.ts +1 -0
  77. package/types/modules/index.d.ts +145 -0
@@ -76,6 +76,28 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
76
76
  return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
77
77
  }
78
78
 
79
+ /**
80
+ * Find entities matching any of the owner/organization pairs, in one query.
81
+ * `organizationId: null` matches rows with no organization (a raw MongoDB
82
+ * `null` also matches a missing field). The caller (a use case) decides
83
+ * which pairs; this only runs them.
84
+ *
85
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
86
+ * @returns {Promise<Array>}
87
+ */
88
+ async findEntitiesByOwnership(clauses) {
89
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
90
+ const docs = await findMany(this.prisma, 'Entity', {
91
+ $or: clauses.map(({ userId, organizationId }) => ({
92
+ userId: toObjectId(userId),
93
+ organizationId:
94
+ organizationId === null ? null : toObjectId(organizationId),
95
+ })),
96
+ });
97
+ const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
98
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
99
+ }
100
+
79
101
  async unsetCredential(entityId) {
80
102
  const objectId = toObjectId(entityId);
81
103
  if (!objectId) return false;
@@ -115,7 +137,14 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
115
137
  );
116
138
  }
117
139
 
118
- async createEntity(entityData) {
140
+ /**
141
+ * @param {Object} entityData
142
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
143
+ * belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
144
+ * because `entityData` carries provider fields and one named `organizationId`
145
+ * must not be taken for ours.
146
+ */
147
+ async createEntity(entityData, tenancy = {}) {
119
148
  const {
120
149
  user,
121
150
  userId,
@@ -133,6 +162,7 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
133
162
  name: name ?? null,
134
163
  moduleName: moduleName ?? null,
135
164
  externalId: externalId ?? null,
165
+ organizationId: toObjectId(tenancy.organizationId) || null,
136
166
  data: dynamicData,
137
167
  };
138
168
  const insertedId = await insertOne(this.prisma, 'Entity', document);
@@ -329,6 +359,14 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
329
359
  if (filter.name) query.name = filter.name;
330
360
  if (filter.moduleName) query.moduleName = filter.moduleName;
331
361
  if (filter.externalId) query.externalId = filter.externalId;
362
+ // `null` means "no organization"; absent means "any". A raw MongoDB
363
+ // `{ field: null }` matches both a null value and a missing field.
364
+ if (filter.organizationId !== undefined) {
365
+ query.organizationId =
366
+ filter.organizationId === null
367
+ ? null
368
+ : toObjectId(filter.organizationId);
369
+ }
332
370
  return query;
333
371
  }
334
372
 
@@ -342,6 +380,8 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
342
380
  externalId: doc?.externalId ?? null,
343
381
  moduleName: doc?.moduleName ?? null,
344
382
  ...dynamicData,
383
+ // After the spread: a provider field of the same name must not decide visibility.
384
+ organizationId: fromObjectId(doc?.organizationId) ?? null,
345
385
  };
346
386
  }
347
387
  }
@@ -67,6 +67,21 @@ class ModuleRepositoryInterface {
67
67
  );
68
68
  }
69
69
 
70
+ /**
71
+ * Find entities matching any of the owner/organization pairs, in one query
72
+ * (ADR-056 §3.6). `organizationId: null` matches rows with no organization.
73
+ * Which pairs is decided by the caller (modules/entity-scope.js), not here.
74
+ *
75
+ * @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
76
+ * @returns {Promise<Array>} Array of entity objects
77
+ * @abstract
78
+ */
79
+ async findEntitiesByOwnership(clauses) {
80
+ throw new Error(
81
+ 'Method findEntitiesByOwnership must be implemented by subclass'
82
+ );
83
+ }
84
+
70
85
  /**
71
86
  * Unset credential from entity
72
87
  *
@@ -110,11 +125,13 @@ class ModuleRepositoryInterface {
110
125
  /**
111
126
  * Create a new entity
112
127
  *
113
- * @param {Object} entityData - Entity data
128
+ * @param {Object} entityData - Entity data (provider fields included)
129
+ * @param {{organizationId?: (string|number|null)}} [tenancy] - the organization
130
+ * the entity belongs to (ADR-056 §3.6), kept apart from `entityData`
114
131
  * @returns {Promise<Object>} Created entity object
115
132
  * @abstract
116
133
  */
117
- async createEntity(entityData) {
134
+ async createEntity(entityData, tenancy) {
118
135
  throw new Error('Method createEntity must be implemented by subclass');
119
136
  }
120
137
 
@@ -1,4 +1,5 @@
1
1
  const { prisma } = require('../../database/prisma');
2
+ const { organizationWhere } = require('../../database/organization-where');
2
3
  const { ModuleRepositoryInterface } = require('./module-repository-interface');
3
4
 
4
5
  /**
@@ -76,6 +77,25 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
76
77
  return credentialMap;
77
78
  }
78
79
 
80
+ /**
81
+ * The one place a stored entity row becomes the entity the application sees.
82
+ * `organizationId` comes after the entity data: a provider field of the same
83
+ * name must not decide who sees the entity.
84
+ * @private
85
+ */
86
+ _mapEntity(row, credential) {
87
+ return {
88
+ id: row.id,
89
+ credential,
90
+ userId: row.userId,
91
+ name: row.name,
92
+ externalId: row.externalId,
93
+ moduleName: row.moduleName,
94
+ ...(row.data || {}),
95
+ organizationId: row.organizationId ?? null,
96
+ };
97
+ }
98
+
79
99
  /**
80
100
  * Find entity by ID with credential
81
101
  * Replaces: Entity.findById(entityId).populate('credential')
@@ -95,15 +115,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
95
115
 
96
116
  const credential = await this._fetchCredential(entity.credentialId);
97
117
 
98
- return {
99
- id: entity.id,
100
- credential,
101
- userId: entity.userId,
102
- name: entity.name,
103
- externalId: entity.externalId,
104
- moduleName: entity.moduleName,
105
- ...(entity.data || {}),
106
- };
118
+ return this._mapEntity(entity, credential);
107
119
  }
108
120
 
109
121
  /**
@@ -121,15 +133,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
121
133
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
122
134
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
123
135
 
124
- return entities.map((e) => ({
125
- id: e.id,
126
- credential: credentialMap.get(e.credentialId) || null,
127
- userId: e.userId,
128
- name: e.name,
129
- externalId: e.externalId,
130
- moduleName: e.moduleName,
131
- ...(e.data || {}),
132
- }));
136
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
133
137
  }
134
138
 
135
139
  /**
@@ -147,15 +151,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
147
151
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
148
152
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
149
153
 
150
- return entities.map((e) => ({
151
- id: e.id,
152
- credential: credentialMap.get(e.credentialId) || null,
153
- userId: e.userId,
154
- name: e.name,
155
- externalId: e.externalId,
156
- moduleName: e.moduleName,
157
- ...(e.data || {}),
158
- }));
154
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
159
155
  }
160
156
 
161
157
  /**
@@ -177,15 +173,32 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
177
173
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
178
174
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
179
175
 
180
- return entities.map((e) => ({
181
- id: e.id,
182
- credential: credentialMap.get(e.credentialId) || null,
183
- userId: e.userId,
184
- name: e.name,
185
- externalId: e.externalId,
186
- moduleName: e.moduleName,
187
- ...(e.data || {}),
188
- }));
176
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
177
+ }
178
+
179
+ /**
180
+ * Find entities matching any of the owner/organization pairs, in one query.
181
+ * `organizationId: null` matches rows with no organization. The caller
182
+ * (a use case) decides which pairs; this only runs them.
183
+ *
184
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
185
+ * @returns {Promise<Array>} Array of entity objects with string IDs
186
+ */
187
+ async findEntitiesByOwnership(clauses) {
188
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
189
+ const entities = await this.prisma.entity.findMany({
190
+ where: {
191
+ OR: clauses.map(({ userId, organizationId }) => ({
192
+ userId,
193
+ ...organizationWhere(organizationId),
194
+ })),
195
+ },
196
+ });
197
+
198
+ const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
199
+ const credentialMap = await this._fetchCredentialsBulk(credentialIds);
200
+
201
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
189
202
  }
190
203
 
191
204
  /**
@@ -223,15 +236,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
223
236
 
224
237
  const credential = await this._fetchCredential(entity.credentialId);
225
238
 
226
- return {
227
- id: entity.id,
228
- credential,
229
- userId: entity.userId,
230
- name: entity.name,
231
- externalId: entity.externalId,
232
- moduleName: entity.moduleName,
233
- ...(entity.data || {}),
234
- };
239
+ return this._mapEntity(entity, credential);
235
240
  }
236
241
 
237
242
  /**
@@ -251,15 +256,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
251
256
  .filter(Boolean);
252
257
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
253
258
 
254
- return entities.map((e) => ({
255
- id: e.id,
256
- credential: credentialMap.get(e.credentialId) || null,
257
- userId: e.userId,
258
- name: e.name,
259
- externalId: e.externalId,
260
- moduleName: e.moduleName,
261
- ...(e.data || {}),
262
- }));
259
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
263
260
  }
264
261
 
265
262
  /**
@@ -267,9 +264,13 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
267
264
  * Replaces: Entity.create(entityData)
268
265
  *
269
266
  * @param {Object} entityData - Entity data
267
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
268
+ * belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
269
+ * because `entityData` carries provider fields and one named `organizationId`
270
+ * must not be taken for ours.
270
271
  * @returns {Promise<Object>} Created entity object with string IDs
271
272
  */
272
- async createEntity(entityData) {
273
+ async createEntity(entityData, tenancy = {}) {
273
274
  const {
274
275
  user,
275
276
  userId,
@@ -287,6 +288,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
287
288
  name,
288
289
  moduleName,
289
290
  externalId,
291
+ organizationId: tenancy.organizationId ?? null,
290
292
  data: dynamicData,
291
293
  };
292
294
 
@@ -296,15 +298,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
296
298
 
297
299
  const credentialObj = await this._fetchCredential(entity.credentialId);
298
300
 
299
- return {
300
- id: entity.id,
301
- credential: credentialObj,
302
- userId: entity.userId,
303
- name: entity.name,
304
- externalId: entity.externalId,
305
- moduleName: entity.moduleName,
306
- ...(entity.data || {}),
307
- };
301
+ return this._mapEntity(entity, credentialObj);
308
302
  }
309
303
 
310
304
  /**
@@ -358,15 +352,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
358
352
 
359
353
  const credentialObj = await this._fetchCredential(entity.credentialId);
360
354
 
361
- return {
362
- id: entity.id,
363
- credential: credentialObj,
364
- userId: entity.userId,
365
- name: entity.name,
366
- externalId: entity.externalId,
367
- moduleName: entity.moduleName,
368
- ...(entity.data || {}),
369
- };
355
+ return this._mapEntity(entity, credentialObj);
370
356
  } catch (error) {
371
357
  if (error.code === 'P2025') {
372
358
  return null;
@@ -429,6 +415,11 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
429
415
  if (filter.moduleName) where.moduleName = filter.moduleName;
430
416
  if (filter.externalId) where.externalId = this._toString(filter.externalId);
431
417
 
418
+ // `null` means "no organization"; absent means "any".
419
+ if (filter.organizationId !== undefined) {
420
+ Object.assign(where, organizationWhere(filter.organizationId));
421
+ }
422
+
432
423
  return where;
433
424
  }
434
425
  }
@@ -108,6 +108,25 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
108
108
  return credentialMap;
109
109
  }
110
110
 
111
+ /**
112
+ * The one place a stored entity row becomes the entity the application sees.
113
+ * `organizationId` comes after the entity data: a provider field of the same
114
+ * name must not decide who sees the entity.
115
+ * @private
116
+ */
117
+ _mapEntity(row, credential) {
118
+ return {
119
+ id: row.id.toString(),
120
+ credential,
121
+ userId: row.userId?.toString(),
122
+ name: row.name,
123
+ externalId: row.externalId,
124
+ moduleName: row.moduleName,
125
+ ...(row.data || {}),
126
+ organizationId: row.organizationId?.toString() ?? null,
127
+ };
128
+ }
129
+
111
130
  /**
112
131
  * Find entity by ID with credential
113
132
  * Replaces: Entity.findById(entityId).populate('credential')
@@ -129,15 +148,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
129
148
 
130
149
  const credential = await this._fetchCredential(entity.credentialId);
131
150
 
132
- return {
133
- id: entity.id.toString(),
134
- credential,
135
- userId: entity.userId?.toString(),
136
- name: entity.name,
137
- externalId: entity.externalId,
138
- moduleName: entity.moduleName,
139
- ...(entity.data || {}),
140
- };
151
+ return this._mapEntity(entity, credential);
141
152
  }
142
153
 
143
154
  /**
@@ -157,15 +168,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
157
168
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
158
169
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
159
170
 
160
- return entities.map((e) => ({
161
- id: e.id.toString(),
162
- credential: credentialMap.get(e.credentialId) || null,
163
- userId: e.userId?.toString(),
164
- name: e.name,
165
- externalId: e.externalId,
166
- moduleName: e.moduleName,
167
- ...(e.data || {}),
168
- }));
171
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
169
172
  }
170
173
 
171
174
  /**
@@ -185,15 +188,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
185
188
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
186
189
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
187
190
 
188
- return entities.map((e) => ({
189
- id: e.id.toString(),
190
- credential: credentialMap.get(e.credentialId) || null,
191
- userId: e.userId?.toString(),
192
- name: e.name,
193
- externalId: e.externalId,
194
- moduleName: e.moduleName,
195
- ...(e.data || {}),
196
- }));
191
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
197
192
  }
198
193
 
199
194
  /**
@@ -217,15 +212,32 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
217
212
  const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
218
213
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
219
214
 
220
- return entities.map((e) => ({
221
- id: e.id.toString(),
222
- credential: credentialMap.get(e.credentialId) || null,
223
- userId: e.userId?.toString(),
224
- name: e.name,
225
- externalId: e.externalId,
226
- moduleName: e.moduleName,
227
- ...(e.data || {}),
228
- }));
215
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
216
+ }
217
+
218
+ /**
219
+ * Find entities matching any of the owner/organization pairs, in one query.
220
+ * `organizationId: null` matches rows with no organization. The caller
221
+ * (a use case) decides which pairs; this only runs them.
222
+ *
223
+ * @param {Array<{userId: string, organizationId: (string|null)}>} clauses
224
+ * @returns {Promise<Array>} Array of entity objects with string IDs
225
+ */
226
+ async findEntitiesByOwnership(clauses) {
227
+ if (!Array.isArray(clauses) || clauses.length === 0) return [];
228
+ const entities = await this.prisma.entity.findMany({
229
+ where: {
230
+ OR: clauses.map(({ userId, organizationId }) => ({
231
+ userId: this._convertId(userId),
232
+ organizationId: this._convertId(organizationId),
233
+ })),
234
+ },
235
+ });
236
+
237
+ const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
238
+ const credentialMap = await this._fetchCredentialsBulk(credentialIds);
239
+
240
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
229
241
  }
230
242
 
231
243
  /**
@@ -264,15 +276,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
264
276
 
265
277
  const credential = await this._fetchCredential(entity.credentialId);
266
278
 
267
- return {
268
- id: entity.id.toString(),
269
- credential,
270
- userId: entity.userId?.toString(),
271
- name: entity.name,
272
- externalId: entity.externalId,
273
- moduleName: entity.moduleName,
274
- ...(entity.data || {}),
275
- };
279
+ return this._mapEntity(entity, credential);
276
280
  }
277
281
 
278
282
  /**
@@ -292,15 +296,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
292
296
  .filter(Boolean);
293
297
  const credentialMap = await this._fetchCredentialsBulk(credentialIds);
294
298
 
295
- return entities.map((e) => ({
296
- id: e.id.toString(),
297
- credential: credentialMap.get(e.credentialId) || null,
298
- userId: e.userId?.toString(),
299
- name: e.name,
300
- externalId: e.externalId,
301
- moduleName: e.moduleName,
302
- ...(e.data || {}),
303
- }));
299
+ return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
304
300
  }
305
301
 
306
302
  /**
@@ -308,9 +304,13 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
308
304
  * Replaces: Entity.create(entityData)
309
305
  *
310
306
  * @param {Object} entityData - Entity data (with string IDs from application layer)
307
+ * @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
308
+ * belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
309
+ * because `entityData` carries provider fields and one named `organizationId`
310
+ * must not be taken for ours.
311
311
  * @returns {Promise<Object>} Created entity object with string IDs
312
312
  */
313
- async createEntity(entityData) {
313
+ async createEntity(entityData, tenancy = {}) {
314
314
  const {
315
315
  user,
316
316
  userId,
@@ -328,6 +328,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
328
328
  name,
329
329
  moduleName,
330
330
  externalId,
331
+ organizationId: this._convertId(tenancy.organizationId ?? null),
331
332
  data: dynamicData,
332
333
  };
333
334
 
@@ -337,15 +338,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
337
338
 
338
339
  const credentialObj = await this._fetchCredential(entity.credentialId);
339
340
 
340
- return {
341
- id: entity.id.toString(),
342
- credential: credentialObj,
343
- userId: entity.userId?.toString(),
344
- name: entity.name,
345
- externalId: entity.externalId,
346
- moduleName: entity.moduleName,
347
- ...(entity.data || {}),
348
- };
341
+ return this._mapEntity(entity, credentialObj);
349
342
  }
350
343
 
351
344
  /**
@@ -401,15 +394,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
401
394
 
402
395
  const credentialObj = await this._fetchCredential(entity.credentialId);
403
396
 
404
- return {
405
- id: entity.id.toString(),
406
- credential: credentialObj,
407
- userId: entity.userId?.toString(),
408
- name: entity.name,
409
- externalId: entity.externalId,
410
- moduleName: entity.moduleName,
411
- ...(entity.data || {}),
412
- };
397
+ return this._mapEntity(entity, credentialObj);
413
398
  } catch (error) {
414
399
  if (error.code === 'P2025') {
415
400
  return null;
@@ -474,6 +459,11 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
474
459
  if (filter.moduleName) where.moduleName = filter.moduleName;
475
460
  if (filter.externalId) where.externalId = this._toString(filter.externalId);
476
461
 
462
+ // `null` means "no organization"; absent means "any".
463
+ if (filter.organizationId !== undefined) {
464
+ where.organizationId = this._convertId(filter.organizationId);
465
+ }
466
+
477
467
  return where;
478
468
  }
479
469
  }