@friggframework/core 2.0.0--canary.680.daae24e.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.
- package/CHANGELOG.md +0 -11
- package/CLAUDE.md +28 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +6 -1
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-config.js +10 -31
- package/database/encryption/encryption-schema-registry.js +99 -47
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +187 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +27 -4
- package/generated/prisma-mongodb/index-browser.js +24 -1
- package/generated/prisma-mongodb/index.d.ts +3227 -47
- package/generated/prisma-mongodb/index.js +27 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +65 -0
- package/generated/prisma-mongodb/wasm.js +27 -4
- package/generated/prisma-postgresql/edge.js +27 -4
- package/generated/prisma-postgresql/index-browser.js +24 -1
- package/generated/prisma-postgresql/index.d.ts +3519 -67
- package/generated/prisma-postgresql/index.js +27 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +61 -0
- package/generated/prisma-postgresql/wasm.js +27 -4
- package/handlers/app-definition-loader.js +103 -12
- package/handlers/routers/auth.js +40 -12
- package/handlers/routers/health.js +4 -2
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/index.js +63 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +5 -0
- package/integrations/integration-router.js +115 -73
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -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/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +7 -1
- package/integrations/use-cases/integration-operations-for-user.js +55 -24
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +44 -1
- package/management-api/management-api-config.js +71 -2
- package/management-api/meta-router.js +21 -2
- package/management-api/openapi/components.js +40 -0
- package/management-api/openapi/v2-operations.js +16 -4
- package/management-api/route-registry.js +123 -6
- package/management-api/v2/dependencies.js +40 -15
- package/management-api/v2/dto.js +20 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/credentials.js +6 -2
- 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/index.js +8 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +50 -13
- package/modules/use-cases/entity-types.js +31 -7
- package/modules/use-cases/execute-entity-proxy-request.js +50 -3
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +34 -7
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +40 -13
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +65 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +61 -0
- package/providers/index.js +23 -0
- package/providers/resolve-provider.js +93 -0
- package/providers/runtime-detection.js +124 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +219 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +193 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +139 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
|
@@ -3,12 +3,14 @@ const {
|
|
|
3
3
|
toObjectId,
|
|
4
4
|
fromObjectId,
|
|
5
5
|
findMany,
|
|
6
|
+
findManyDrained,
|
|
6
7
|
findOne,
|
|
7
8
|
insertOne,
|
|
8
9
|
updateOne,
|
|
9
10
|
deleteOne,
|
|
10
11
|
} = require('../../database/documentdb-utils');
|
|
11
12
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
13
|
+
const { entityNotFoundError } = require('./entity-not-found');
|
|
12
14
|
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -33,11 +35,11 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
33
35
|
async findEntityById(entityId) {
|
|
34
36
|
const objectId = toObjectId(entityId);
|
|
35
37
|
if (!objectId) {
|
|
36
|
-
throw
|
|
38
|
+
throw entityNotFoundError(entityId);
|
|
37
39
|
}
|
|
38
40
|
const doc = await findOne(this.prisma, 'Entity', { _id: objectId });
|
|
39
41
|
if (!doc) {
|
|
40
|
-
throw
|
|
42
|
+
throw entityNotFoundError(entityId);
|
|
41
43
|
}
|
|
42
44
|
const credential = await this._fetchCredential(doc.credentialId);
|
|
43
45
|
return this._mapEntity(doc, credential);
|
|
@@ -76,6 +78,29 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
76
78
|
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Find entities matching any of the owner/organization pairs, in one query.
|
|
83
|
+
* `organizationId: null` matches rows with no organization (a raw MongoDB
|
|
84
|
+
* `null` also matches a missing field). The caller (a use case) decides
|
|
85
|
+
* which pairs; this only runs them.
|
|
86
|
+
*
|
|
87
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
88
|
+
* @returns {Promise<Array>}
|
|
89
|
+
*/
|
|
90
|
+
async findEntitiesByOwnership(clauses) {
|
|
91
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
92
|
+
// Drained: a list can hold more than the first cursor batch (~101).
|
|
93
|
+
const docs = await findManyDrained(this.prisma, 'Entity', {
|
|
94
|
+
$or: clauses.map(({ userId, organizationId }) => ({
|
|
95
|
+
userId: toObjectId(userId),
|
|
96
|
+
organizationId:
|
|
97
|
+
organizationId === null ? null : toObjectId(organizationId),
|
|
98
|
+
})),
|
|
99
|
+
});
|
|
100
|
+
const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
|
|
101
|
+
return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
async unsetCredential(entityId) {
|
|
80
105
|
const objectId = toObjectId(entityId);
|
|
81
106
|
if (!objectId) return false;
|
|
@@ -115,7 +140,14 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
115
140
|
);
|
|
116
141
|
}
|
|
117
142
|
|
|
118
|
-
|
|
143
|
+
/**
|
|
144
|
+
* @param {Object} entityData
|
|
145
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
146
|
+
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
147
|
+
* because `entityData` carries provider fields and one named `organizationId`
|
|
148
|
+
* must not be taken for ours.
|
|
149
|
+
*/
|
|
150
|
+
async createEntity(entityData, tenancy = {}) {
|
|
119
151
|
const {
|
|
120
152
|
user,
|
|
121
153
|
userId,
|
|
@@ -133,6 +165,7 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
133
165
|
name: name ?? null,
|
|
134
166
|
moduleName: moduleName ?? null,
|
|
135
167
|
externalId: externalId ?? null,
|
|
168
|
+
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
136
169
|
data: dynamicData,
|
|
137
170
|
};
|
|
138
171
|
const insertedId = await insertOne(this.prisma, 'Entity', document);
|
|
@@ -247,8 +280,9 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
247
280
|
const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
|
|
248
281
|
if (objectIds.length === 0) return new Map();
|
|
249
282
|
|
|
250
|
-
//
|
|
251
|
-
|
|
283
|
+
// Raw read to bypass the Prisma encryption extension. Drained:
|
|
284
|
+
// more ids than the first cursor batch must all come back.
|
|
285
|
+
const rawCredentials = await findManyDrained(this.prisma, 'Credential', {
|
|
252
286
|
_id: { $in: objectIds }
|
|
253
287
|
});
|
|
254
288
|
|
|
@@ -329,19 +363,30 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
329
363
|
if (filter.name) query.name = filter.name;
|
|
330
364
|
if (filter.moduleName) query.moduleName = filter.moduleName;
|
|
331
365
|
if (filter.externalId) query.externalId = filter.externalId;
|
|
366
|
+
// `null` means "no organization"; absent means "any". A raw MongoDB
|
|
367
|
+
// `{ field: null }` matches both a null value and a missing field.
|
|
368
|
+
if (filter.organizationId !== undefined) {
|
|
369
|
+
query.organizationId =
|
|
370
|
+
filter.organizationId === null
|
|
371
|
+
? null
|
|
372
|
+
: toObjectId(filter.organizationId);
|
|
373
|
+
}
|
|
332
374
|
return query;
|
|
333
375
|
}
|
|
334
376
|
|
|
335
377
|
_mapEntity(doc, credential) {
|
|
336
378
|
const dynamicData = doc?.data || {};
|
|
337
379
|
return {
|
|
380
|
+
// First, so no stored provider field can replace a column below
|
|
381
|
+
// (identity, owner or organization decide who sees the entity).
|
|
382
|
+
...dynamicData,
|
|
338
383
|
id: fromObjectId(doc?._id),
|
|
339
384
|
credential,
|
|
340
385
|
userId: fromObjectId(doc?.userId),
|
|
341
386
|
name: doc?.name ?? null,
|
|
342
387
|
externalId: doc?.externalId ?? null,
|
|
343
388
|
moduleName: doc?.moduleName ?? null,
|
|
344
|
-
|
|
389
|
+
organizationId: fromObjectId(doc?.organizationId) ?? null,
|
|
345
390
|
};
|
|
346
391
|
}
|
|
347
392
|
}
|
|
@@ -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,5 +1,7 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { organizationWhere } = require('../../database/organization-where');
|
|
2
3
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
4
|
+
const { entityNotFoundError } = require('./entity-not-found');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* MongoDB Module Repository Adapter
|
|
@@ -76,6 +78,26 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
76
78
|
return credentialMap;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The one place a stored entity row becomes the entity the application sees.
|
|
83
|
+
* The provider's `data` is spread FIRST and every column is pinned after it:
|
|
84
|
+
* a stored `id`, `userId` or `organizationId` in `data` must not decide who
|
|
85
|
+
* owns the entity or who sees it.
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
_mapEntity(row, credential) {
|
|
89
|
+
return {
|
|
90
|
+
...(row.data || {}),
|
|
91
|
+
id: row.id,
|
|
92
|
+
credential,
|
|
93
|
+
userId: row.userId,
|
|
94
|
+
name: row.name,
|
|
95
|
+
externalId: row.externalId,
|
|
96
|
+
moduleName: row.moduleName,
|
|
97
|
+
organizationId: row.organizationId ?? null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
/**
|
|
80
102
|
* Find entity by ID with credential
|
|
81
103
|
* Replaces: Entity.findById(entityId).populate('credential')
|
|
@@ -90,20 +112,12 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
90
112
|
});
|
|
91
113
|
|
|
92
114
|
if (!entity) {
|
|
93
|
-
throw
|
|
115
|
+
throw entityNotFoundError(entityId);
|
|
94
116
|
}
|
|
95
117
|
|
|
96
118
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
97
119
|
|
|
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
|
-
};
|
|
120
|
+
return this._mapEntity(entity, credential);
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
/**
|
|
@@ -121,15 +135,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
121
135
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
122
136
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
123
137
|
|
|
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
|
-
}));
|
|
138
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
/**
|
|
@@ -147,15 +153,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
147
153
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
148
154
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
149
155
|
|
|
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
|
-
}));
|
|
156
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
/**
|
|
@@ -177,15 +175,32 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
177
175
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
178
176
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
179
177
|
|
|
180
|
-
return entities.map((e) => (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Find entities matching any of the owner/organization pairs, in one query.
|
|
183
|
+
* `organizationId: null` matches rows with no organization. The caller
|
|
184
|
+
* (a use case) decides which pairs; this only runs them.
|
|
185
|
+
*
|
|
186
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
187
|
+
* @returns {Promise<Array>} Array of entity objects with string IDs
|
|
188
|
+
*/
|
|
189
|
+
async findEntitiesByOwnership(clauses) {
|
|
190
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
191
|
+
const entities = await this.prisma.entity.findMany({
|
|
192
|
+
where: {
|
|
193
|
+
OR: clauses.map(({ userId, organizationId }) => ({
|
|
194
|
+
userId,
|
|
195
|
+
...organizationWhere(organizationId),
|
|
196
|
+
})),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
201
|
+
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
202
|
+
|
|
203
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
189
204
|
}
|
|
190
205
|
|
|
191
206
|
/**
|
|
@@ -223,15 +238,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
223
238
|
|
|
224
239
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
225
240
|
|
|
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
|
-
};
|
|
241
|
+
return this._mapEntity(entity, credential);
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
/**
|
|
@@ -251,15 +258,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
251
258
|
.filter(Boolean);
|
|
252
259
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
253
260
|
|
|
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
|
-
}));
|
|
261
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
/**
|
|
@@ -267,9 +266,13 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
267
266
|
* Replaces: Entity.create(entityData)
|
|
268
267
|
*
|
|
269
268
|
* @param {Object} entityData - Entity data
|
|
269
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
270
|
+
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
271
|
+
* because `entityData` carries provider fields and one named `organizationId`
|
|
272
|
+
* must not be taken for ours.
|
|
270
273
|
* @returns {Promise<Object>} Created entity object with string IDs
|
|
271
274
|
*/
|
|
272
|
-
async createEntity(entityData) {
|
|
275
|
+
async createEntity(entityData, tenancy = {}) {
|
|
273
276
|
const {
|
|
274
277
|
user,
|
|
275
278
|
userId,
|
|
@@ -287,6 +290,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
287
290
|
name,
|
|
288
291
|
moduleName,
|
|
289
292
|
externalId,
|
|
293
|
+
organizationId: tenancy.organizationId ?? null,
|
|
290
294
|
data: dynamicData,
|
|
291
295
|
};
|
|
292
296
|
|
|
@@ -296,15 +300,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
296
300
|
|
|
297
301
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
298
302
|
|
|
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
|
-
};
|
|
303
|
+
return this._mapEntity(entity, credentialObj);
|
|
308
304
|
}
|
|
309
305
|
|
|
310
306
|
/**
|
|
@@ -358,15 +354,7 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
358
354
|
|
|
359
355
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
360
356
|
|
|
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
|
-
};
|
|
357
|
+
return this._mapEntity(entity, credentialObj);
|
|
370
358
|
} catch (error) {
|
|
371
359
|
if (error.code === 'P2025') {
|
|
372
360
|
return null;
|
|
@@ -429,6 +417,11 @@ class ModuleRepositoryMongo extends ModuleRepositoryInterface {
|
|
|
429
417
|
if (filter.moduleName) where.moduleName = filter.moduleName;
|
|
430
418
|
if (filter.externalId) where.externalId = this._toString(filter.externalId);
|
|
431
419
|
|
|
420
|
+
// `null` means "no organization"; absent means "any".
|
|
421
|
+
if (filter.organizationId !== undefined) {
|
|
422
|
+
Object.assign(where, organizationWhere(filter.organizationId));
|
|
423
|
+
}
|
|
424
|
+
|
|
432
425
|
return where;
|
|
433
426
|
}
|
|
434
427
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
3
|
+
const { entityNotFoundError } = require('./entity-not-found');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* PostgreSQL Module Repository Adapter
|
|
@@ -108,6 +109,26 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
108
109
|
return credentialMap;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The one place a stored entity row becomes the entity the application sees.
|
|
114
|
+
* The provider's `data` is spread FIRST and every column is pinned after it:
|
|
115
|
+
* a stored `id`, `userId` or `organizationId` in `data` must not decide who
|
|
116
|
+
* owns the entity or who sees it.
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
_mapEntity(row, credential) {
|
|
120
|
+
return {
|
|
121
|
+
...(row.data || {}),
|
|
122
|
+
id: row.id.toString(),
|
|
123
|
+
credential,
|
|
124
|
+
userId: row.userId?.toString(),
|
|
125
|
+
name: row.name,
|
|
126
|
+
externalId: row.externalId,
|
|
127
|
+
moduleName: row.moduleName,
|
|
128
|
+
organizationId: row.organizationId?.toString() ?? null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
/**
|
|
112
133
|
* Find entity by ID with credential
|
|
113
134
|
* Replaces: Entity.findById(entityId).populate('credential')
|
|
@@ -124,20 +145,12 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
124
145
|
});
|
|
125
146
|
|
|
126
147
|
if (!entity) {
|
|
127
|
-
throw
|
|
148
|
+
throw entityNotFoundError(entityId);
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
131
152
|
|
|
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
|
-
};
|
|
153
|
+
return this._mapEntity(entity, credential);
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
/**
|
|
@@ -157,15 +170,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
157
170
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
158
171
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
159
172
|
|
|
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
|
-
}));
|
|
173
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
/**
|
|
@@ -185,15 +190,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
185
190
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
186
191
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
187
192
|
|
|
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
|
-
}));
|
|
193
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
197
194
|
}
|
|
198
195
|
|
|
199
196
|
/**
|
|
@@ -217,15 +214,32 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
217
214
|
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
218
215
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
219
216
|
|
|
220
|
-
return entities.map((e) => (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Find entities matching any of the owner/organization pairs, in one query.
|
|
222
|
+
* `organizationId: null` matches rows with no organization. The caller
|
|
223
|
+
* (a use case) decides which pairs; this only runs them.
|
|
224
|
+
*
|
|
225
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
226
|
+
* @returns {Promise<Array>} Array of entity objects with string IDs
|
|
227
|
+
*/
|
|
228
|
+
async findEntitiesByOwnership(clauses) {
|
|
229
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
230
|
+
const entities = await this.prisma.entity.findMany({
|
|
231
|
+
where: {
|
|
232
|
+
OR: clauses.map(({ userId, organizationId }) => ({
|
|
233
|
+
userId: this._convertId(userId),
|
|
234
|
+
organizationId: this._convertId(organizationId),
|
|
235
|
+
})),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
|
|
240
|
+
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
241
|
+
|
|
242
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
/**
|
|
@@ -264,15 +278,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
264
278
|
|
|
265
279
|
const credential = await this._fetchCredential(entity.credentialId);
|
|
266
280
|
|
|
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
|
-
};
|
|
281
|
+
return this._mapEntity(entity, credential);
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
/**
|
|
@@ -292,15 +298,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
292
298
|
.filter(Boolean);
|
|
293
299
|
const credentialMap = await this._fetchCredentialsBulk(credentialIds);
|
|
294
300
|
|
|
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
|
-
}));
|
|
301
|
+
return entities.map((e) => (this._mapEntity(e, credentialMap.get(e.credentialId) || null)));
|
|
304
302
|
}
|
|
305
303
|
|
|
306
304
|
/**
|
|
@@ -308,9 +306,13 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
308
306
|
* Replaces: Entity.create(entityData)
|
|
309
307
|
*
|
|
310
308
|
* @param {Object} entityData - Entity data (with string IDs from application layer)
|
|
309
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the entity
|
|
310
|
+
* belongs to (ADR-056 §3.6). A separate argument, not a field of `entityData`,
|
|
311
|
+
* because `entityData` carries provider fields and one named `organizationId`
|
|
312
|
+
* must not be taken for ours.
|
|
311
313
|
* @returns {Promise<Object>} Created entity object with string IDs
|
|
312
314
|
*/
|
|
313
|
-
async createEntity(entityData) {
|
|
315
|
+
async createEntity(entityData, tenancy = {}) {
|
|
314
316
|
const {
|
|
315
317
|
user,
|
|
316
318
|
userId,
|
|
@@ -328,6 +330,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
328
330
|
name,
|
|
329
331
|
moduleName,
|
|
330
332
|
externalId,
|
|
333
|
+
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
331
334
|
data: dynamicData,
|
|
332
335
|
};
|
|
333
336
|
|
|
@@ -337,15 +340,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
337
340
|
|
|
338
341
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
339
342
|
|
|
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
|
-
};
|
|
343
|
+
return this._mapEntity(entity, credentialObj);
|
|
349
344
|
}
|
|
350
345
|
|
|
351
346
|
/**
|
|
@@ -401,15 +396,7 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
401
396
|
|
|
402
397
|
const credentialObj = await this._fetchCredential(entity.credentialId);
|
|
403
398
|
|
|
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
|
-
};
|
|
399
|
+
return this._mapEntity(entity, credentialObj);
|
|
413
400
|
} catch (error) {
|
|
414
401
|
if (error.code === 'P2025') {
|
|
415
402
|
return null;
|
|
@@ -474,6 +461,11 @@ class ModuleRepositoryPostgres extends ModuleRepositoryInterface {
|
|
|
474
461
|
if (filter.moduleName) where.moduleName = filter.moduleName;
|
|
475
462
|
if (filter.externalId) where.externalId = this._toString(filter.externalId);
|
|
476
463
|
|
|
464
|
+
// `null` means "no organization"; absent means "any".
|
|
465
|
+
if (filter.organizationId !== undefined) {
|
|
466
|
+
where.organizationId = this._convertId(filter.organizationId);
|
|
467
|
+
}
|
|
468
|
+
|
|
477
469
|
return where;
|
|
478
470
|
}
|
|
479
471
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
3
|
+
const { entityNotFoundError } = require('./entity-not-found');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Prisma-based Module Repository
|
|
@@ -37,7 +38,7 @@ class ModuleRepository extends ModuleRepositoryInterface {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
if (!entity) {
|
|
40
|
-
throw
|
|
41
|
+
throw entityNotFoundError(entityId);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
return {
|