@friggframework/core 2.0.0--canary.679.a2bcc2f.0 → 2.0.0--canary.686.87e9875.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 +25 -0
- package/README.md +105 -0
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/user-commands.js +17 -5
- package/core/Worker.js +31 -7
- 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/encryption-schema-registry.js +80 -12
- 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 +167 -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 +93 -12
- package/handlers/routers/auth.js +40 -12
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/index.js +58 -1
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +36 -7
- 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/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 +55 -24
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/management-api/management-api-config.js +71 -2
- package/management-api/openapi/components.js +40 -0
- package/management-api/openapi/v2-operations.js +16 -4
- 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 +35 -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-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +32 -5
- package/modules/use-cases/get-entity-options-by-id.js +9 -3
- package/modules/use-cases/get-module.js +9 -3
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +39 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +9 -3
- package/modules/use-cases/test-module-auth.js +9 -3
- 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 +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +76 -0
- package/queues/providers/qstash-queue-provider.js +122 -0
- package/queues/providers/sqs-queue-provider.js +61 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +46 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +129 -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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error `findIntegrationById` throws for an id with no row, and the one
|
|
3
|
+
* predicate that recognises it. The adapters build the error here and the
|
|
4
|
+
* use cases match it here, so the message is written once and a missing
|
|
5
|
+
* integration can be told from any other failure without widening a regex.
|
|
6
|
+
*/
|
|
7
|
+
const MESSAGE = /^Integration with id .+ not found$/;
|
|
8
|
+
|
|
9
|
+
const integrationNotFoundError = (id) =>
|
|
10
|
+
new Error(`Integration with id ${id} not found`);
|
|
11
|
+
|
|
12
|
+
const isIntegrationNotFound = (error) => MESSAGE.test(error?.message || '');
|
|
13
|
+
|
|
14
|
+
module.exports = { integrationNotFoundError, isIntegrationNotFound };
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
IntegrationRepositoryInterface,
|
|
15
15
|
} = require('./integration-repository-interface');
|
|
16
16
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
17
|
+
const { integrationNotFoundError } = require('./integration-not-found');
|
|
17
18
|
|
|
18
19
|
class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
19
20
|
constructor() {
|
|
@@ -28,6 +29,28 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
28
29
|
return records.map((doc) => this._mapIntegration(doc));
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
34
|
+
* query. `organizationId: null` matches rows with no organization (a raw
|
|
35
|
+
* MongoDB `null` also matches a missing field). The caller (a use case)
|
|
36
|
+
* decides which pairs; this only runs them.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
39
|
+
* @returns {Promise<Array>}
|
|
40
|
+
*/
|
|
41
|
+
async findIntegrationsByOwnership(clauses) {
|
|
42
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
43
|
+
// Drained: a list can hold more than the first cursor batch (~101).
|
|
44
|
+
const records = await findManyDrained(this.prisma, 'Integration', {
|
|
45
|
+
$or: clauses.map(({ userId, organizationId }) => ({
|
|
46
|
+
userId: toObjectId(userId),
|
|
47
|
+
organizationId:
|
|
48
|
+
organizationId === null ? null : toObjectId(organizationId),
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
return records.map((doc) => this._mapIntegration(doc));
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
async findIntegrations({ type, status } = {}) {
|
|
32
55
|
const filter = {};
|
|
33
56
|
if (type) {
|
|
@@ -68,11 +91,11 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
68
91
|
async findIntegrationById(id) {
|
|
69
92
|
const objectId = toObjectId(id);
|
|
70
93
|
if (!objectId) {
|
|
71
|
-
throw
|
|
94
|
+
throw integrationNotFoundError(id);
|
|
72
95
|
}
|
|
73
96
|
const doc = await findOne(this.prisma, 'Integration', { _id: objectId });
|
|
74
97
|
if (!doc) {
|
|
75
|
-
throw
|
|
98
|
+
throw integrationNotFoundError(id);
|
|
76
99
|
}
|
|
77
100
|
return this._mapIntegration(doc);
|
|
78
101
|
}
|
|
@@ -132,10 +155,18 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
132
155
|
return true;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
|
-
|
|
158
|
+
/**
|
|
159
|
+
* @param {Array<string>} entities - entity ids
|
|
160
|
+
* @param {string} userId - owner (the person, or the organization's user)
|
|
161
|
+
* @param {Object} config - Integration configuration
|
|
162
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
163
|
+
* integration is created in (ADR-056 §3.6)
|
|
164
|
+
*/
|
|
165
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
136
166
|
const now = new Date();
|
|
137
167
|
const document = {
|
|
138
168
|
userId: toObjectId(userId) || null,
|
|
169
|
+
organizationId: toObjectId(tenancy.organizationId) || null,
|
|
139
170
|
config,
|
|
140
171
|
version: '0.0.0',
|
|
141
172
|
status: 'IN_CREATION',
|
|
@@ -298,6 +329,7 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
298
329
|
id: fromObjectId(doc?._id),
|
|
299
330
|
entitiesIds: (doc?.entityIds || []).map((value) => fromObjectId(value)),
|
|
300
331
|
userId: fromObjectId(doc?.userId),
|
|
332
|
+
organizationId: fromObjectId(doc?.organizationId) ?? null,
|
|
301
333
|
config: doc?.config ?? null,
|
|
302
334
|
version: doc?.version ?? null,
|
|
303
335
|
status: doc?.status ?? null,
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
* @abstract
|
|
11
11
|
*/
|
|
12
12
|
class IntegrationRepositoryInterface {
|
|
13
|
+
/**
|
|
14
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
15
|
+
* query (ADR-056 §3.6). `organizationId: null` matches rows with no
|
|
16
|
+
* organization. Which pairs is decided by the caller
|
|
17
|
+
* (modules/entity-scope.js), not here.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<{userId: string|number, organizationId: (string|number|null)}>} clauses
|
|
20
|
+
* @returns {Promise<Array>} Array of integration objects
|
|
21
|
+
* @abstract
|
|
22
|
+
*/
|
|
23
|
+
async findIntegrationsByOwnership(clauses) {
|
|
24
|
+
throw new Error('Method findIntegrationsByOwnership must be implemented by subclass');
|
|
25
|
+
}
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Find all integrations for a user
|
|
15
29
|
*
|
|
@@ -123,10 +137,12 @@ class IntegrationRepositoryInterface {
|
|
|
123
137
|
* @param {Array<string|number>} entities - Array of entity IDs
|
|
124
138
|
* @param {string|number} userId - User ID
|
|
125
139
|
* @param {Object} config - Integration configuration
|
|
140
|
+
* @param {{organizationId?: (string|number|null)}} [tenancy] - the organization the
|
|
141
|
+
* integration is created in (ADR-056 §3.6)
|
|
126
142
|
* @returns {Promise<Object>} Created integration object
|
|
127
143
|
* @abstract
|
|
128
144
|
*/
|
|
129
|
-
async createIntegration(entities, userId, config) {
|
|
145
|
+
async createIntegration(entities, userId, config, tenancy) {
|
|
130
146
|
throw new Error('Method createIntegration must be implemented by subclass');
|
|
131
147
|
}
|
|
132
148
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { organizationWhere } = require('../../database/organization-where');
|
|
2
3
|
const {
|
|
3
4
|
IntegrationRepositoryInterface,
|
|
4
5
|
} = require('./integration-repository-interface');
|
|
5
6
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
7
|
+
const { integrationNotFoundError } = require('./integration-not-found');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* MongoDB Integration Repository Adapter
|
|
@@ -25,6 +27,25 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
25
27
|
this.prisma = prisma;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The one place a integration integration row becomes the record the
|
|
32
|
+
* application sees (ids of its entities, its organization).
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
_mapIntegration(integration) {
|
|
36
|
+
return {
|
|
37
|
+
id: integration.id,
|
|
38
|
+
entitiesIds: integration.entities.map((e) => e.id),
|
|
39
|
+
userId: integration.userId,
|
|
40
|
+
organizationId: integration.organizationId ?? null,
|
|
41
|
+
config: integration.config,
|
|
42
|
+
version: integration.version,
|
|
43
|
+
status: integration.status,
|
|
44
|
+
messages: integration.messages,
|
|
45
|
+
createdAt: integration.createdAt,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
/**
|
|
29
50
|
* Find all integrations for a user
|
|
30
51
|
* Replaces: IntegrationModel.find({ user: userId }).populate('entities')
|
|
@@ -41,16 +62,32 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
41
62
|
});
|
|
42
63
|
|
|
43
64
|
// Map to domain objects (maintains same API)
|
|
44
|
-
return integrations.map((integration) => (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
return integrations.map((integration) => (this._mapIntegration(integration)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
70
|
+
* query. `organizationId: null` matches rows with no organization. The
|
|
71
|
+
* caller (a use case) decides which pairs; this only runs them.
|
|
72
|
+
*
|
|
73
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
74
|
+
* @returns {Promise<Array>} Array of integration objects
|
|
75
|
+
*/
|
|
76
|
+
async findIntegrationsByOwnership(clauses) {
|
|
77
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
78
|
+
const integrations = await this.prisma.integration.findMany({
|
|
79
|
+
where: {
|
|
80
|
+
OR: clauses.map(({ userId, organizationId }) => ({
|
|
81
|
+
userId,
|
|
82
|
+
...organizationWhere(organizationId),
|
|
83
|
+
})),
|
|
84
|
+
},
|
|
85
|
+
include: {
|
|
86
|
+
entities: true,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return integrations.map((integration) => (this._mapIntegration(integration)));
|
|
54
91
|
}
|
|
55
92
|
|
|
56
93
|
/**
|
|
@@ -77,16 +114,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
77
114
|
},
|
|
78
115
|
});
|
|
79
116
|
|
|
80
|
-
return integrations.map((integration) => (
|
|
81
|
-
id: integration.id,
|
|
82
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
83
|
-
userId: integration.userId,
|
|
84
|
-
config: integration.config,
|
|
85
|
-
version: integration.version,
|
|
86
|
-
status: integration.status,
|
|
87
|
-
messages: integration.messages,
|
|
88
|
-
createdAt: integration.createdAt,
|
|
89
|
-
}));
|
|
117
|
+
return integrations.map((integration) => (this._mapIntegration(integration)));
|
|
90
118
|
}
|
|
91
119
|
|
|
92
120
|
/**
|
|
@@ -165,15 +193,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
165
193
|
throw new Error(`Integration with name ${name} not found`);
|
|
166
194
|
}
|
|
167
195
|
|
|
168
|
-
return
|
|
169
|
-
id: integration.id,
|
|
170
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
171
|
-
userId: integration.userId,
|
|
172
|
-
config: integration.config,
|
|
173
|
-
version: integration.version,
|
|
174
|
-
status: integration.status,
|
|
175
|
-
messages: integration.messages,
|
|
176
|
-
};
|
|
196
|
+
return this._mapIntegration(integration);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
/**
|
|
@@ -192,18 +212,10 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
192
212
|
});
|
|
193
213
|
|
|
194
214
|
if (!integration) {
|
|
195
|
-
throw
|
|
215
|
+
throw integrationNotFoundError(id);
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
return
|
|
199
|
-
id: integration.id,
|
|
200
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
201
|
-
userId: integration.userId,
|
|
202
|
-
config: integration.config,
|
|
203
|
-
version: integration.version,
|
|
204
|
-
status: integration.status,
|
|
205
|
-
messages: integration.messages,
|
|
206
|
-
};
|
|
218
|
+
return this._mapIntegration(integration);
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
/**
|
|
@@ -290,15 +302,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
290
302
|
},
|
|
291
303
|
});
|
|
292
304
|
|
|
293
|
-
return integrations.map((integration) => (
|
|
294
|
-
id: integration.id,
|
|
295
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
296
|
-
userId: integration.userId,
|
|
297
|
-
config: integration.config,
|
|
298
|
-
version: integration.version,
|
|
299
|
-
status: integration.status,
|
|
300
|
-
messages: integration.messages,
|
|
301
|
-
}));
|
|
305
|
+
return integrations.map((integration) => (this._mapIntegration(integration)));
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
/**
|
|
@@ -310,13 +314,16 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
310
314
|
* @param {Array<string>} entities - Array of entity IDs (MongoDB ObjectIds)
|
|
311
315
|
* @param {string} userId - User ID (MongoDB ObjectId)
|
|
312
316
|
* @param {Object} config - Integration configuration
|
|
317
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
318
|
+
* integration is created in (ADR-056 §3.6)
|
|
313
319
|
* @returns {Promise<Object>} Created integration object
|
|
314
320
|
*/
|
|
315
|
-
async createIntegration(entities, userId, config) {
|
|
321
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
316
322
|
const data = {
|
|
317
323
|
config,
|
|
318
324
|
version: '0.0.0',
|
|
319
325
|
userId: userId,
|
|
326
|
+
organizationId: tenancy.organizationId ?? null,
|
|
320
327
|
entityIds: entities,
|
|
321
328
|
};
|
|
322
329
|
|
|
@@ -327,15 +334,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
327
334
|
},
|
|
328
335
|
});
|
|
329
336
|
|
|
330
|
-
return
|
|
331
|
-
id: integration.id,
|
|
332
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
333
|
-
userId: integration.userId,
|
|
334
|
-
config: integration.config,
|
|
335
|
-
version: integration.version,
|
|
336
|
-
status: integration.status,
|
|
337
|
-
messages: integration.messages,
|
|
338
|
-
};
|
|
337
|
+
return this._mapIntegration(integration);
|
|
339
338
|
}
|
|
340
339
|
|
|
341
340
|
/**
|
|
@@ -357,15 +356,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
357
356
|
return null;
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
return
|
|
361
|
-
id: integration.id,
|
|
362
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
363
|
-
userId: integration.userId,
|
|
364
|
-
config: integration.config,
|
|
365
|
-
version: integration.version,
|
|
366
|
-
status: integration.status,
|
|
367
|
-
messages: integration.messages,
|
|
368
|
-
};
|
|
359
|
+
return this._mapIntegration(integration);
|
|
369
360
|
}
|
|
370
361
|
|
|
371
362
|
/**
|
|
@@ -389,15 +380,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
389
380
|
},
|
|
390
381
|
});
|
|
391
382
|
|
|
392
|
-
return
|
|
393
|
-
id: integration.id,
|
|
394
|
-
entitiesIds: integration.entities.map((e) => e.id),
|
|
395
|
-
userId: integration.userId,
|
|
396
|
-
config: integration.config,
|
|
397
|
-
version: integration.version,
|
|
398
|
-
status: integration.status,
|
|
399
|
-
messages: integration.messages,
|
|
400
|
-
};
|
|
383
|
+
return this._mapIntegration(integration);
|
|
401
384
|
}
|
|
402
385
|
|
|
403
386
|
/**
|
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
IntegrationRepositoryInterface,
|
|
4
4
|
} = require('./integration-repository-interface');
|
|
5
5
|
const { validateConfigPatch } = require('./config-patch-shared');
|
|
6
|
+
const { integrationNotFoundError } = require('./integration-not-found');
|
|
6
7
|
const { strictIntId } = require('./report-id');
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -51,6 +52,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
51
52
|
...integration,
|
|
52
53
|
id: integration.id?.toString(),
|
|
53
54
|
userId: integration.userId?.toString(),
|
|
55
|
+
organizationId: integration.organizationId?.toString() ?? null,
|
|
54
56
|
entities: integration.entities?.map(e => ({
|
|
55
57
|
...e,
|
|
56
58
|
id: e.id?.toString(),
|
|
@@ -60,6 +62,25 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The one place a converted integration row becomes the record the
|
|
67
|
+
* application sees (string ids, ids of its entities, its organization).
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
_mapIntegration(converted) {
|
|
71
|
+
return {
|
|
72
|
+
id: converted.id,
|
|
73
|
+
entitiesIds: converted.entities.map((e) => e.id),
|
|
74
|
+
userId: converted.userId,
|
|
75
|
+
organizationId: converted.organizationId,
|
|
76
|
+
config: converted.config,
|
|
77
|
+
version: converted.version,
|
|
78
|
+
status: converted.status,
|
|
79
|
+
messages: converted.messages,
|
|
80
|
+
createdAt: converted.createdAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
/**
|
|
64
85
|
* Find all integrations for a user
|
|
65
86
|
*
|
|
@@ -78,16 +99,35 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
78
99
|
// Map to domain objects with string IDs
|
|
79
100
|
return integrations.map((integration) => {
|
|
80
101
|
const converted = this._convertIntegrationIds(integration);
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
return this._mapIntegration(converted);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find integrations matching any of the owner/organization pairs, in one
|
|
108
|
+
* query. `organizationId: null` matches rows with no organization. The
|
|
109
|
+
* caller (a use case) decides which pairs; this only runs them.
|
|
110
|
+
*
|
|
111
|
+
* @param {Array<{userId: string, organizationId: (string|null)}>} clauses
|
|
112
|
+
* @returns {Promise<Array>} Array of integration objects with string IDs
|
|
113
|
+
*/
|
|
114
|
+
async findIntegrationsByOwnership(clauses) {
|
|
115
|
+
if (!Array.isArray(clauses) || clauses.length === 0) return [];
|
|
116
|
+
const integrations = await this.prisma.integration.findMany({
|
|
117
|
+
where: {
|
|
118
|
+
OR: clauses.map(({ userId, organizationId }) => ({
|
|
119
|
+
userId: this._convertId(userId),
|
|
120
|
+
organizationId: this._convertId(organizationId),
|
|
121
|
+
})),
|
|
122
|
+
},
|
|
123
|
+
include: {
|
|
124
|
+
entities: true,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return integrations.map((integration) => {
|
|
129
|
+
const converted = this._convertIntegrationIds(integration);
|
|
130
|
+
return this._mapIntegration(converted);
|
|
91
131
|
});
|
|
92
132
|
}
|
|
93
133
|
|
|
@@ -117,16 +157,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
117
157
|
|
|
118
158
|
return integrations.map((integration) => {
|
|
119
159
|
const converted = this._convertIntegrationIds(integration);
|
|
120
|
-
return
|
|
121
|
-
id: converted.id,
|
|
122
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
123
|
-
userId: converted.userId,
|
|
124
|
-
config: converted.config,
|
|
125
|
-
version: converted.version,
|
|
126
|
-
status: converted.status,
|
|
127
|
-
messages: converted.messages,
|
|
128
|
-
createdAt: converted.createdAt,
|
|
129
|
-
};
|
|
160
|
+
return this._mapIntegration(converted);
|
|
130
161
|
});
|
|
131
162
|
}
|
|
132
163
|
|
|
@@ -209,15 +240,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
209
240
|
}
|
|
210
241
|
|
|
211
242
|
const converted = this._convertIntegrationIds(integration);
|
|
212
|
-
return
|
|
213
|
-
id: converted.id,
|
|
214
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
215
|
-
userId: converted.userId,
|
|
216
|
-
config: converted.config,
|
|
217
|
-
version: converted.version,
|
|
218
|
-
status: converted.status,
|
|
219
|
-
messages: converted.messages,
|
|
220
|
-
};
|
|
243
|
+
return this._mapIntegration(converted);
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
/**
|
|
@@ -236,19 +259,11 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
236
259
|
});
|
|
237
260
|
|
|
238
261
|
if (!integration) {
|
|
239
|
-
throw
|
|
262
|
+
throw integrationNotFoundError(id);
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
const converted = this._convertIntegrationIds(integration);
|
|
243
|
-
return
|
|
244
|
-
id: converted.id,
|
|
245
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
246
|
-
userId: converted.userId,
|
|
247
|
-
config: converted.config,
|
|
248
|
-
version: converted.version,
|
|
249
|
-
status: converted.status,
|
|
250
|
-
messages: converted.messages,
|
|
251
|
-
};
|
|
266
|
+
return this._mapIntegration(converted);
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
/**
|
|
@@ -328,12 +343,15 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
328
343
|
* @param {Array<string>} entities - Array of entity IDs (strings from application layer)
|
|
329
344
|
* @param {string} userId - User ID (string from application layer)
|
|
330
345
|
* @param {Object} config - Integration configuration
|
|
346
|
+
* @param {{organizationId?: (string|null)}} [tenancy] - the organization the
|
|
347
|
+
* integration is created in (ADR-056 §3.6)
|
|
331
348
|
* @returns {Promise<Object>} Created integration object with string IDs
|
|
332
349
|
*/
|
|
333
|
-
async createIntegration(entities, userId, config) {
|
|
350
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
334
351
|
const data = {
|
|
335
352
|
config,
|
|
336
353
|
version: '0.0.0',
|
|
354
|
+
organizationId: this._convertId(tenancy.organizationId ?? null),
|
|
337
355
|
};
|
|
338
356
|
|
|
339
357
|
// PostgreSQL: use nested relations with ID conversion
|
|
@@ -354,15 +372,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
354
372
|
});
|
|
355
373
|
|
|
356
374
|
const converted = this._convertIntegrationIds(integration);
|
|
357
|
-
return
|
|
358
|
-
id: converted.id,
|
|
359
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
360
|
-
userId: converted.userId,
|
|
361
|
-
config: converted.config,
|
|
362
|
-
version: converted.version,
|
|
363
|
-
status: converted.status,
|
|
364
|
-
messages: converted.messages,
|
|
365
|
-
};
|
|
375
|
+
return this._mapIntegration(converted);
|
|
366
376
|
}
|
|
367
377
|
|
|
368
378
|
/**
|
|
@@ -385,15 +395,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
const converted = this._convertIntegrationIds(integration);
|
|
388
|
-
return
|
|
389
|
-
id: converted.id,
|
|
390
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
391
|
-
userId: converted.userId,
|
|
392
|
-
config: converted.config,
|
|
393
|
-
version: converted.version,
|
|
394
|
-
status: converted.status,
|
|
395
|
-
messages: converted.messages,
|
|
396
|
-
};
|
|
398
|
+
return this._mapIntegration(converted);
|
|
397
399
|
}
|
|
398
400
|
|
|
399
401
|
/**
|
|
@@ -418,15 +420,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
418
420
|
});
|
|
419
421
|
|
|
420
422
|
const converted = this._convertIntegrationIds(integration);
|
|
421
|
-
return
|
|
422
|
-
id: converted.id,
|
|
423
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
424
|
-
userId: converted.userId,
|
|
425
|
-
config: converted.config,
|
|
426
|
-
version: converted.version,
|
|
427
|
-
status: converted.status,
|
|
428
|
-
messages: converted.messages,
|
|
429
|
-
};
|
|
423
|
+
return this._mapIntegration(converted);
|
|
430
424
|
}
|
|
431
425
|
|
|
432
426
|
/**
|
|
@@ -476,15 +470,7 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
476
470
|
|
|
477
471
|
return integrations.map((integration) => {
|
|
478
472
|
const converted = this._convertIntegrationIds(integration);
|
|
479
|
-
return
|
|
480
|
-
id: converted.id,
|
|
481
|
-
entitiesIds: converted.entities.map((e) => e.id),
|
|
482
|
-
userId: converted.userId,
|
|
483
|
-
config: converted.config,
|
|
484
|
-
version: converted.version,
|
|
485
|
-
status: converted.status,
|
|
486
|
-
messages: converted.messages,
|
|
487
|
-
};
|
|
473
|
+
return this._mapIntegration(converted);
|
|
488
474
|
});
|
|
489
475
|
}
|
|
490
476
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { v4: uuid } = require('uuid');
|
|
2
2
|
const { validateConfigPatch } = require('../../repositories/config-patch-shared');
|
|
3
|
+
const { matchesAnyClause } = require('../../../modules/tests/doubles/ownership-matching');
|
|
3
4
|
|
|
4
5
|
class TestIntegrationRepository {
|
|
5
6
|
constructor() {
|
|
@@ -7,13 +8,14 @@ class TestIntegrationRepository {
|
|
|
7
8
|
this.operationHistory = [];
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
async createIntegration(entities, userId, config) {
|
|
11
|
+
async createIntegration(entities, userId, config, tenancy = {}) {
|
|
11
12
|
const id = uuid();
|
|
12
13
|
const record = {
|
|
13
14
|
id,
|
|
14
15
|
_id: id,
|
|
15
16
|
entitiesIds: entities,
|
|
16
17
|
userId: userId,
|
|
18
|
+
organizationId: tenancy.organizationId ?? null,
|
|
17
19
|
config,
|
|
18
20
|
version: '0.0.0',
|
|
19
21
|
status: 'IN_CREATION',
|
|
@@ -38,6 +40,15 @@ class TestIntegrationRepository {
|
|
|
38
40
|
return results;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// Answers the way a database does for the clauses a use case hands it.
|
|
44
|
+
async findIntegrationsByOwnership(clauses) {
|
|
45
|
+
const results = Array.from(this.store.values()).filter((r) =>
|
|
46
|
+
matchesAnyClause(r, clauses)
|
|
47
|
+
);
|
|
48
|
+
this.operationHistory.push({ operation: 'findByOwnership', clauses, count: results.length });
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
async findIntegrations(filter = {}) {
|
|
42
53
|
const { type, status } = filter;
|
|
43
54
|
const results = Array.from(this.store.values()).filter((r) => {
|