@friggframework/core 2.0.0--canary.704.79d9a63.0 → 2.0.0--canary.686.e94de4d.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +28 -40
- package/README.md +105 -0
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +20 -73
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-encryption-service.js +9 -22
- package/database/documentdb-utils.js +2 -0
- package/database/encryption/README.md +76 -14
- package/database/encryption/documentdb-encryption-service.md +78 -1
- package/database/encryption/encryption-config.js +219 -0
- package/database/encryption/encryption-schema-registry.js +93 -12
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/organization-where.js +18 -0
- package/database/prisma.js +15 -38
- package/database/unique-violation.js +25 -0
- package/database/use-cases/check-encryption-health-use-case.js +43 -52
- package/errors/index.js +0 -4
- 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 +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +121 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +13 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +65 -3
- 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 +1 -0
- package/integrations/integration-base.js +0 -29
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +17 -31
- package/integrations/repositories/integration-repository-mongo.js +60 -129
- package/integrations/repositories/integration-repository-postgres.js +62 -134
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/schedule-config.js +80 -0
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +72 -75
- 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 +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +76 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +408 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +411 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-errors.js +20 -0
- package/modules/proxy/proxy-path.js +179 -0
- package/modules/proxy/proxy-policy.js +248 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- 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/requester/proxy-request.js +244 -0
- package/modules/requester/requester.js +27 -2
- 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/in-memory-authorization-session-repository.js +45 -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 +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +286 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- 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 +329 -0
- 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/modules/utils/map-module-dto.js +3 -5
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -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 +87 -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 +2 -0
- package/types/modules/index.d.ts +197 -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/tests/doubles/test-user-repository.js +1 -1
- 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-bearer-token.js +2 -7
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -92
- package/integrations/use-cases/remove-entities-from-integration.js +0 -70
- package/integrations/utils/assert-entity-ids-input.js +0 -42
- package/modules/utils/to-credential-id.js +0 -14
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for persisting AuthorizationSession records (Management API v2
|
|
3
|
+
* multi-step authorization). Adapters return AuthorizationSession domain
|
|
4
|
+
* objects and never expired sessions from lookups.
|
|
5
|
+
*/
|
|
6
|
+
class AuthorizationSessionRepositoryInterface {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('../domain/authorization-session').AuthorizationSession} session
|
|
9
|
+
* @returns {Promise<import('../domain/authorization-session').AuthorizationSession>}
|
|
10
|
+
*/
|
|
11
|
+
async create(session) {
|
|
12
|
+
throw new Error('Method create must be implemented by subclass');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} sessionId
|
|
17
|
+
* @returns {Promise<import('../domain/authorization-session').AuthorizationSession|null>}
|
|
18
|
+
* null when missing or expired.
|
|
19
|
+
*/
|
|
20
|
+
async findBySessionId(sessionId) {
|
|
21
|
+
throw new Error('Method findBySessionId must be implemented by subclass');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Saves currentStep, stepData and completed.
|
|
26
|
+
* @param {import('../domain/authorization-session').AuthorizationSession} session
|
|
27
|
+
*/
|
|
28
|
+
async update(session) {
|
|
29
|
+
throw new Error('Method update must be implemented by subclass');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {string} sessionId */
|
|
33
|
+
async deleteBySessionId(sessionId) {
|
|
34
|
+
throw new Error('Method deleteBySessionId must be implemented by subclass');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Removes sessions that expired before `now`.
|
|
39
|
+
* @param {Date} [now]
|
|
40
|
+
* @returns {Promise<number>} how many were removed
|
|
41
|
+
*/
|
|
42
|
+
async deleteExpired(now) {
|
|
43
|
+
throw new Error('Method deleteExpired must be implemented by subclass');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { AuthorizationSessionRepositoryInterface };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { AuthorizationSession } = require('../domain/authorization-session');
|
|
3
|
+
const {
|
|
4
|
+
AuthorizationSessionRepositoryInterface,
|
|
5
|
+
} = require('./authorization-session-repository-interface');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* AuthorizationSession persistence through the Prisma client, for PostgreSQL
|
|
9
|
+
* and MongoDB (both schemas declare the same model; the session id, not the
|
|
10
|
+
* row id, is the key, so no id conversion is needed). `stepData` is
|
|
11
|
+
* encrypted by the Prisma encryption extension (encryption-schema-registry).
|
|
12
|
+
*/
|
|
13
|
+
class AuthorizationSessionRepositoryPrisma extends AuthorizationSessionRepositoryInterface {
|
|
14
|
+
constructor(prismaClient = prisma) {
|
|
15
|
+
super();
|
|
16
|
+
this.prisma = prismaClient;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async create(session) {
|
|
20
|
+
const record = await this.prisma.authorizationSession.create({
|
|
21
|
+
data: {
|
|
22
|
+
sessionId: session.sessionId,
|
|
23
|
+
userId: session.userId,
|
|
24
|
+
entityType: session.entityType,
|
|
25
|
+
credentialId: session.credentialId,
|
|
26
|
+
currentStep: session.currentStep,
|
|
27
|
+
maxSteps: session.maxSteps,
|
|
28
|
+
stepData: session.stepData,
|
|
29
|
+
expiresAt: session.expiresAt,
|
|
30
|
+
completed: session.completed,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return toDomain(record);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findBySessionId(sessionId) {
|
|
37
|
+
if (typeof sessionId !== 'string' || !sessionId) return null;
|
|
38
|
+
const record = await this.prisma.authorizationSession.findFirst({
|
|
39
|
+
where: { sessionId, expiresAt: { gt: new Date() } },
|
|
40
|
+
});
|
|
41
|
+
return record ? toDomain(record) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async update(session) {
|
|
45
|
+
const record = await this.prisma.authorizationSession.update({
|
|
46
|
+
where: { sessionId: session.sessionId },
|
|
47
|
+
data: {
|
|
48
|
+
currentStep: session.currentStep,
|
|
49
|
+
stepData: session.stepData,
|
|
50
|
+
completed: session.completed,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return toDomain(record);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async deleteBySessionId(sessionId) {
|
|
57
|
+
const result = await this.prisma.authorizationSession.deleteMany({
|
|
58
|
+
where: { sessionId },
|
|
59
|
+
});
|
|
60
|
+
return result.count;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async deleteExpired(now = new Date()) {
|
|
64
|
+
const result = await this.prisma.authorizationSession.deleteMany({
|
|
65
|
+
where: { expiresAt: { lte: now } },
|
|
66
|
+
});
|
|
67
|
+
return result.count;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toDomain(record) {
|
|
72
|
+
return new AuthorizationSession({
|
|
73
|
+
sessionId: record.sessionId,
|
|
74
|
+
userId: record.userId,
|
|
75
|
+
entityType: record.entityType,
|
|
76
|
+
credentialId: record.credentialId ?? null,
|
|
77
|
+
currentStep: record.currentStep,
|
|
78
|
+
maxSteps: record.maxSteps,
|
|
79
|
+
stepData: record.stepData || {},
|
|
80
|
+
expiresAt: record.expiresAt,
|
|
81
|
+
completed: record.completed,
|
|
82
|
+
createdAt: record.createdAt,
|
|
83
|
+
updatedAt: record.updatedAt,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { AuthorizationSessionRepositoryPrisma, toDomain };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error `findEntityById` throws for an id with no row, and the one
|
|
3
|
+
* predicate that recognises it (as integration-not-found.js does for
|
|
4
|
+
* integrations). The adapters build the error here and use cases match it
|
|
5
|
+
* here, so a missing entity can be told from any other failure without
|
|
6
|
+
* widening a regex.
|
|
7
|
+
*
|
|
8
|
+
* Why it matters: a use case that answers 404 for another tenant's entity must
|
|
9
|
+
* answer the same 404 for an id that does not exist. Otherwise the status code
|
|
10
|
+
* says which ids exist (ADR-052 §3, ADR-056 §3.6).
|
|
11
|
+
*/
|
|
12
|
+
const MESSAGE = /^Entity .+ not found$/;
|
|
13
|
+
|
|
14
|
+
const entityNotFoundError = (id) => new Error(`Entity ${id} not found`);
|
|
15
|
+
|
|
16
|
+
const isEntityNotFound = (error) => MESSAGE.test(error?.message || '');
|
|
17
|
+
|
|
18
|
+
module.exports = { entityNotFoundError, isEntityNotFound };
|
|
@@ -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
|
}
|