@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.
- package/README.md +36 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- package/database/encryption/encryption-schema-registry.js +7 -0
- package/database/organization-where.js +18 -0
- package/generated/prisma-mongodb/edge.js +5 -3
- package/generated/prisma-mongodb/index-browser.js +2 -0
- package/generated/prisma-mongodb/index.d.ts +115 -2
- package/generated/prisma-mongodb/index.js +5 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +15 -0
- package/generated/prisma-mongodb/wasm.js +5 -3
- package/generated/prisma-postgresql/edge.js +5 -3
- package/generated/prisma-postgresql/index-browser.js +2 -0
- package/generated/prisma-postgresql/index.d.ts +131 -2
- package/generated/prisma-postgresql/index.js +5 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +15 -0
- package/generated/prisma-postgresql/wasm.js +5 -3
- package/handlers/app-definition-loader.js +25 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +10 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +99 -52
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +34 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -1
- package/integrations/use-cases/integration-operations-for-user.js +40 -19
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/management-api-config.js +3 -0
- package/management-api/openapi/components.js +46 -1
- package/management-api/openapi/v2-operations.js +14 -1
- package/management-api/v2/dependencies.js +56 -14
- package/management-api/v2/dto.js +17 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/repositories/module-repository-documentdb.js +41 -1
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +64 -73
- package/modules/repositories/module-repository-postgres.js +63 -73
- package/modules/scope-config.js +155 -0
- package/modules/scope-policy.js +80 -0
- package/modules/scope-resolution.js +277 -0
- package/modules/scope-visibility.js +165 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +59 -0
- package/modules/use-cases/entity-operations-for-user.js +42 -10
- package/modules/use-cases/entity-types.js +62 -14
- package/modules/use-cases/execute-entity-proxy-request.js +15 -0
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +23 -3
- package/modules/use-cases/get-entity-options-by-id.js +5 -2
- package/modules/use-cases/get-module.js +5 -2
- package/modules/use-cases/multi-step-authorization.js +36 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +5 -2
- package/modules/use-cases/test-module-auth.js +5 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +15 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/schema.prisma +15 -0
- package/types/index.d.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
}
|