@friggframework/core 2.0.0--canary.710.91495fa.0 → 2.0.0--canary.702.ef0dc8c.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 +1 -41
- package/application/commands/README.md +11 -17
- package/application/commands/entity-commands.js +60 -18
- package/application/commands/integration-commands.js +0 -63
- package/database/encryption/README.md +3 -3
- package/database/encryption/encryption-schema-registry.js +7 -27
- package/handlers/app-definition-loader.js +9 -2
- package/integrations/repositories/integration-repository-documentdb.js +0 -32
- package/integrations/repositories/integration-repository-interface.js +0 -30
- package/integrations/repositories/integration-repository-mongo.js +0 -52
- package/integrations/repositories/integration-repository-postgres.js +0 -58
- package/integrations/use-cases/create-integration.js +19 -57
- package/jest-setup.js +2 -13
- package/jest-teardown.js +2 -5
- package/jest.config.js +0 -4
- package/modules/utils/map-module-dto.js +3 -5
- package/package.json +8 -8
- package/queues/queuer-util.js +5 -6
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/get-user-from-bearer-token.js +2 -7
- 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/jest.db.config.js +0 -23
- package/modules/utils/to-credential-id.js +0 -14
package/CLAUDE.md
CHANGED
|
@@ -206,51 +206,11 @@ packages/core/
|
|
|
206
206
|
- `delete-integration-for-user.js` - Remove integration
|
|
207
207
|
- `get-integration-instance.js` - Load integration with modules
|
|
208
208
|
- `load-integration-context.js` - Full integration context loading
|
|
209
|
-
- `add-entities-to-integration.js` / `remove-entities-from-integration.js` - Attach or detach entities on an existing integration
|
|
210
209
|
|
|
211
210
|
**Repositories**:
|
|
212
211
|
- `integration-repository-factory.js` - Creates database-specific repositories
|
|
213
212
|
- `integration-repository-mongo.js` - MongoDB implementation
|
|
214
213
|
- `integration-repository-postgres.js` - PostgreSQL implementation
|
|
215
|
-
- `addEntitiesToIntegration(integrationId, entityIds)` /
|
|
216
|
-
`removeEntitiesFromIntegration(integrationId, entityIds)` change the entity
|
|
217
|
-
set of an existing integration (all three adapters; `null` for an unknown
|
|
218
|
-
integration). Reach them through the `addEntitiesToIntegration` /
|
|
219
|
-
`removeEntitiesFromIntegration` commands. The add use case
|
|
220
|
-
(`add-entities-to-integration.js`) rejects unknown entities
|
|
221
|
-
(`ENTITIES_NOT_FOUND`, 404), entities of another user
|
|
222
|
-
(`ENTITY_USER_MISMATCH`, 403) and entities of a module the integration does
|
|
223
|
-
not already hold (`ENTITY_MODULE_MISMATCH`, 400). The remove use case
|
|
224
|
-
(`remove-entities-from-integration.js`) refuses to leave a module with no
|
|
225
|
-
entity (`LAST_ENTITY_OF_MODULE`, 409). Re-adding an attached entity or
|
|
226
|
-
removing a detached one changes nothing; a detached entity is not deleted.
|
|
227
|
-
`integration-repository-entities-parity.db.test.js` runs every adapter against
|
|
228
|
-
real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
|
|
229
|
-
`INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
|
|
230
|
-
writes only `Integration.entityIds` (as its `createIntegration` does), not
|
|
231
|
-
`Entity.integrationIds`.
|
|
232
|
-
**One credential, many accounts**: when one credential covers several
|
|
233
|
-
provider accounts, give each account its own entity (externalId = account
|
|
234
|
-
id) and attach them all to one integration; webhooks then route with
|
|
235
|
-
`findIntegrationByEntityExternalId`. Traps:
|
|
236
|
-
(1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
|
|
237
|
-
holds only one entity's module, and on the queue/webhook path
|
|
238
|
-
(`get-integration-instance.js`) `this.entities` holds entity **ids**, not
|
|
239
|
-
records. For per-account work (a webhook for one account, teardown of every
|
|
240
|
-
account in `onDelete`), load
|
|
241
|
-
`this.commands.loadIntegrationContextById(this.id)` and use
|
|
242
|
-
`context.modules`: one module per entity, each with its record on
|
|
243
|
-
`module.entity` (pick by `module.entity.externalId`).
|
|
244
|
-
(2) `findIntegrationByEntityExternalId` counts every entity with that
|
|
245
|
-
externalId, attached or not, across users: reuse an existing entity
|
|
246
|
-
(`findEntity`) when an account comes back, or delete it when it is revoked,
|
|
247
|
-
never create a second one.
|
|
248
|
-
(3) `createIntegration` reuses an existing integration of the same user
|
|
249
|
-
and type whose entities include all requested ones (an exact match wins),
|
|
250
|
-
so a reconnect that sends the original entities finds the integration that
|
|
251
|
-
now holds more.
|
|
252
|
-
(4) Every module's delegate is the integration: an auth failure on any
|
|
253
|
-
account marks the shared credential invalid and the integration ERROR.
|
|
254
214
|
- `integration-mapping-repository-*.js` - Mapping data persistence.
|
|
255
215
|
`queryMappings(integrationId, { where, orderBy, skip, take, omit })`
|
|
256
216
|
returns `{ mappings, total }`: one filtered, ordered page of an
|
|
@@ -266,7 +226,7 @@ packages/core/
|
|
|
266
226
|
mapping keys to leave out of the rows; never write such rows back. Path
|
|
267
227
|
segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
|
|
268
228
|
DocumentDB adapters give the same pages and totals;
|
|
269
|
-
`integration-mapping-repository-query-parity.
|
|
229
|
+
`integration-mapping-repository-query-parity.test.js` checks that against
|
|
270
230
|
real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
|
|
271
231
|
`QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
|
|
272
232
|
strings compare by the database collation on PostgreSQL and by code point
|
|
@@ -67,10 +67,9 @@ class MyIntegration extends IntegrationBase {
|
|
|
67
67
|
async hydrateFromExternalUser(externalUserId) {
|
|
68
68
|
// Find integration context by external entity ID
|
|
69
69
|
const result =
|
|
70
|
-
await this.commands.findIntegrationContextByExternalEntityId(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
70
|
+
await this.commands.findIntegrationContextByExternalEntityId(
|
|
71
|
+
externalUserId
|
|
72
|
+
);
|
|
74
73
|
|
|
75
74
|
if (result.error) {
|
|
76
75
|
return { error: result.error };
|
|
@@ -232,10 +231,9 @@ Manage integration records and load full integration contexts.
|
|
|
232
231
|
```javascript
|
|
233
232
|
// Find integration context by external entity ID
|
|
234
233
|
// Returns { context, error } where context includes record + hydrated modules
|
|
235
|
-
const result = await commands.findIntegrationContextByExternalEntityId(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
234
|
+
const result = await commands.findIntegrationContextByExternalEntityId(
|
|
235
|
+
'external-user-or-workspace-id'
|
|
236
|
+
);
|
|
239
237
|
|
|
240
238
|
if (!result.error) {
|
|
241
239
|
integration.setIntegrationRecord(result.context);
|
|
@@ -301,10 +299,7 @@ try {
|
|
|
301
299
|
For integration context operations, errors are returned in the result:
|
|
302
300
|
|
|
303
301
|
```javascript
|
|
304
|
-
const result = await commands.findIntegrationContextByExternalEntityId(
|
|
305
|
-
externalId: userId,
|
|
306
|
-
type: 'my-integration',
|
|
307
|
-
});
|
|
302
|
+
const result = await commands.findIntegrationContextByExternalEntityId(userId);
|
|
308
303
|
|
|
309
304
|
if (result.error) {
|
|
310
305
|
return { error: result.error };
|
|
@@ -354,10 +349,9 @@ const result =
|
|
|
354
349
|
const { createFriggCommands } = require('@friggframework/core');
|
|
355
350
|
|
|
356
351
|
this.commands = createFriggCommands({ integrationClass: MyIntegration });
|
|
357
|
-
const result = await this.commands.findIntegrationContextByExternalEntityId(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
});
|
|
352
|
+
const result = await this.commands.findIntegrationContextByExternalEntityId(
|
|
353
|
+
userId
|
|
354
|
+
);
|
|
361
355
|
```
|
|
362
356
|
|
|
363
357
|
## Best Practices
|
|
@@ -439,7 +433,7 @@ if (!user) {
|
|
|
439
433
|
| | `findEntitiesByIds(ids)` | Find multiple by IDs |
|
|
440
434
|
| | `updateEntity(id, updates)` | Update entity properties |
|
|
441
435
|
| | `deleteEntity(id)` | Delete entity |
|
|
442
|
-
| **Integration** | `findIntegrationContextByExternalEntityId(
|
|
436
|
+
| **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
|
|
443
437
|
| | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
|
|
444
438
|
|
|
445
439
|
## Support
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createModuleRepository,
|
|
3
3
|
} = require('../../modules/repositories/module-repository-factory');
|
|
4
|
-
const { toCredentialId } = require('../../modules/utils/to-credential-id');
|
|
5
4
|
|
|
6
5
|
const ERROR_CODE_MAP = {
|
|
7
6
|
ENTITY_NOT_FOUND: 404,
|
|
8
7
|
INVALID_ENTITY_DATA: 400,
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
function toEntityDto(entity) {
|
|
12
|
-
return {
|
|
13
|
-
id: entity.id,
|
|
14
|
-
userId: entity.userId,
|
|
15
|
-
externalId: entity.externalId,
|
|
16
|
-
name: entity.name,
|
|
17
|
-
moduleName: entity.moduleName,
|
|
18
|
-
credentialId: toCredentialId(entity.credential),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
10
|
function mapErrorToResponse(error) {
|
|
23
11
|
const status = ERROR_CODE_MAP[error?.code] || 500;
|
|
24
12
|
return {
|
|
@@ -78,7 +66,16 @@ function createEntityCommands() {
|
|
|
78
66
|
|
|
79
67
|
const entity = await moduleRepo.createEntity(entityData);
|
|
80
68
|
|
|
81
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
id: entity.id,
|
|
71
|
+
userId: entity.userId,
|
|
72
|
+
externalId: entity.externalId,
|
|
73
|
+
name: entity.name,
|
|
74
|
+
moduleName: entity.moduleName,
|
|
75
|
+
credentialId: entity.credential?._id
|
|
76
|
+
? entity.credential._id.toString()
|
|
77
|
+
: entity.credential,
|
|
78
|
+
};
|
|
82
79
|
} catch (error) {
|
|
83
80
|
return mapErrorToResponse(error);
|
|
84
81
|
}
|
|
@@ -112,7 +109,16 @@ function createEntityCommands() {
|
|
|
112
109
|
return null;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
id: entity.id,
|
|
114
|
+
userId: entity.userId,
|
|
115
|
+
externalId: entity.externalId,
|
|
116
|
+
name: entity.name,
|
|
117
|
+
moduleName: entity.moduleName,
|
|
118
|
+
credentialId: entity.credential?._id
|
|
119
|
+
? entity.credential._id.toString()
|
|
120
|
+
: entity.credential,
|
|
121
|
+
};
|
|
116
122
|
} catch (error) {
|
|
117
123
|
return mapErrorToResponse(error);
|
|
118
124
|
}
|
|
@@ -133,7 +139,16 @@ function createEntityCommands() {
|
|
|
133
139
|
|
|
134
140
|
const entities = await moduleRepo.findEntitiesByUserId(userId);
|
|
135
141
|
|
|
136
|
-
return entities.map((entity) =>
|
|
142
|
+
return entities.map((entity) => ({
|
|
143
|
+
id: entity.id,
|
|
144
|
+
userId: entity.userId,
|
|
145
|
+
externalId: entity.externalId,
|
|
146
|
+
name: entity.name,
|
|
147
|
+
moduleName: entity.moduleName,
|
|
148
|
+
credentialId: entity.credential?._id
|
|
149
|
+
? entity.credential._id.toString()
|
|
150
|
+
: entity.credential,
|
|
151
|
+
}));
|
|
137
152
|
} catch (error) {
|
|
138
153
|
if (error.code) {
|
|
139
154
|
return mapErrorToResponse(error);
|
|
@@ -165,7 +180,16 @@ function createEntityCommands() {
|
|
|
165
180
|
moduleName
|
|
166
181
|
);
|
|
167
182
|
|
|
168
|
-
return entities.map((entity) =>
|
|
183
|
+
return entities.map((entity) => ({
|
|
184
|
+
id: entity.id,
|
|
185
|
+
userId: entity.userId,
|
|
186
|
+
externalId: entity.externalId,
|
|
187
|
+
name: entity.name,
|
|
188
|
+
moduleName: entity.moduleName,
|
|
189
|
+
credentialId: entity.credential?._id
|
|
190
|
+
? entity.credential._id.toString()
|
|
191
|
+
: entity.credential,
|
|
192
|
+
}));
|
|
169
193
|
} catch (error) {
|
|
170
194
|
if (error.code) {
|
|
171
195
|
return mapErrorToResponse(error);
|
|
@@ -189,7 +213,16 @@ function createEntityCommands() {
|
|
|
189
213
|
|
|
190
214
|
const entity = await moduleRepo.findEntityById(entityId);
|
|
191
215
|
|
|
192
|
-
return
|
|
216
|
+
return {
|
|
217
|
+
id: entity.id,
|
|
218
|
+
userId: entity.userId,
|
|
219
|
+
externalId: entity.externalId,
|
|
220
|
+
name: entity.name,
|
|
221
|
+
moduleName: entity.moduleName,
|
|
222
|
+
credentialId: entity.credential?._id
|
|
223
|
+
? entity.credential._id.toString()
|
|
224
|
+
: entity.credential,
|
|
225
|
+
};
|
|
193
226
|
} catch (error) {
|
|
194
227
|
return mapErrorToResponse(error);
|
|
195
228
|
}
|
|
@@ -217,7 +250,16 @@ function createEntityCommands() {
|
|
|
217
250
|
throw error;
|
|
218
251
|
}
|
|
219
252
|
|
|
220
|
-
return
|
|
253
|
+
return {
|
|
254
|
+
id: entity.id,
|
|
255
|
+
userId: entity.userId,
|
|
256
|
+
externalId: entity.externalId,
|
|
257
|
+
name: entity.name,
|
|
258
|
+
moduleName: entity.moduleName,
|
|
259
|
+
credentialId: entity.credential?._id
|
|
260
|
+
? entity.credential._id.toString()
|
|
261
|
+
: entity.credential,
|
|
262
|
+
};
|
|
221
263
|
} catch (error) {
|
|
222
264
|
return mapErrorToResponse(error);
|
|
223
265
|
}
|
|
@@ -29,12 +29,6 @@ const {
|
|
|
29
29
|
const {
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
|
-
const {
|
|
33
|
-
AddEntitiesToIntegration,
|
|
34
|
-
} = require('../../integrations/use-cases/add-entities-to-integration');
|
|
35
|
-
const {
|
|
36
|
-
RemoveEntitiesFromIntegration,
|
|
37
|
-
} = require('../../integrations/use-cases/remove-entities-from-integration');
|
|
38
32
|
const {
|
|
39
33
|
getModulesDefinitionFromIntegrationClasses,
|
|
40
34
|
} = require('../../integrations/utils/map-integration-dto');
|
|
@@ -46,11 +40,6 @@ const ERROR_CODE_MAP = {
|
|
|
46
40
|
EXTERNAL_ID_REQUIRED: 400,
|
|
47
41
|
TYPE_REQUIRED: 400,
|
|
48
42
|
INTEGRATION_RECORD_NOT_FOUND: 404,
|
|
49
|
-
INVALID_INTEGRATION_DATA: 400,
|
|
50
|
-
ENTITIES_NOT_FOUND: 404,
|
|
51
|
-
ENTITY_USER_MISMATCH: 403,
|
|
52
|
-
ENTITY_MODULE_MISMATCH: 400,
|
|
53
|
-
LAST_ENTITY_OF_MODULE: 409,
|
|
54
43
|
};
|
|
55
44
|
|
|
56
45
|
function mapErrorToResponse(error) {
|
|
@@ -174,17 +163,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
174
163
|
integrationRepository,
|
|
175
164
|
});
|
|
176
165
|
|
|
177
|
-
const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
|
|
178
|
-
integrationRepository,
|
|
179
|
-
moduleRepository,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const removeEntitiesFromIntegrationUseCase =
|
|
183
|
-
new RemoveEntitiesFromIntegration({
|
|
184
|
-
integrationRepository,
|
|
185
|
-
moduleRepository,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
166
|
return {
|
|
189
167
|
findIntegrationById,
|
|
190
168
|
listIntegrations,
|
|
@@ -329,47 +307,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
329
307
|
}
|
|
330
308
|
},
|
|
331
309
|
|
|
332
|
-
/**
|
|
333
|
-
* Attach entities to an existing integration. Every entity must
|
|
334
|
-
* exist, belong to the integration's user, and be of a module the
|
|
335
|
-
* integration already holds; entities that are already attached are
|
|
336
|
-
* left as they are.
|
|
337
|
-
* @param {Object} params
|
|
338
|
-
* @param {string} params.integrationId - Integration ID
|
|
339
|
-
* @param {Array<string>} params.entityIds - Entity IDs to attach
|
|
340
|
-
* @returns {Promise<Object>} Updated integration
|
|
341
|
-
*/
|
|
342
|
-
async addEntitiesToIntegration({ integrationId, entityIds }) {
|
|
343
|
-
try {
|
|
344
|
-
return await addEntitiesToIntegrationUseCase.execute(
|
|
345
|
-
integrationId,
|
|
346
|
-
entityIds
|
|
347
|
-
);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
return mapErrorToResponse(error);
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Detach entities from an integration. Entities that are not
|
|
355
|
-
* attached are ignored; detached entities are not deleted. Each
|
|
356
|
-
* module must keep at least one entity.
|
|
357
|
-
* @param {Object} params
|
|
358
|
-
* @param {string} params.integrationId - Integration ID
|
|
359
|
-
* @param {Array<string>} params.entityIds - Entity IDs to detach
|
|
360
|
-
* @returns {Promise<Object>} Updated integration
|
|
361
|
-
*/
|
|
362
|
-
async removeEntitiesFromIntegration({ integrationId, entityIds }) {
|
|
363
|
-
try {
|
|
364
|
-
return await removeEntitiesFromIntegrationUseCase.execute(
|
|
365
|
-
integrationId,
|
|
366
|
-
entityIds
|
|
367
|
-
);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
return mapErrorToResponse(error);
|
|
370
|
-
}
|
|
371
|
-
},
|
|
372
|
-
|
|
373
310
|
/**
|
|
374
311
|
* Delete an integration by ID
|
|
375
312
|
* @param {string} integrationId - Integration ID to delete
|
|
@@ -508,9 +508,9 @@ return entities.map((e) => ({
|
|
|
508
508
|
|
|
509
509
|
**Verified:**
|
|
510
510
|
|
|
511
|
-
- ✅ `postgres-relation-decryption.
|
|
512
|
-
- ✅ `postgres-decryption-fix-verification.
|
|
513
|
-
- ✅ `mongo-decryption-fix-verification.
|
|
511
|
+
- ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
|
|
512
|
+
- ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
|
|
513
|
+
- ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
|
|
514
514
|
|
|
515
515
|
**Implementation Examples:**
|
|
516
516
|
|
|
@@ -174,46 +174,26 @@ function loadModuleEncryptionSchemas(integrations) {
|
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
const { getModulesDefinitionFromIntegrationClasses } = require('
|
|
177
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
(integration) => integration?.Definition?.modules
|
|
182
|
-
);
|
|
183
|
-
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(
|
|
184
|
-
integrationsWithModules
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// Fields the core schema already encrypts (e.g. data.api_key) are
|
|
188
|
-
// covered; registering them again would fail validation and drop every
|
|
189
|
-
// other module field with them.
|
|
190
|
-
const coreCredentialFields = CORE_ENCRYPTION_SCHEMA.Credential.fields;
|
|
191
|
-
const credentialFields = extractCredentialFieldsFromModules(
|
|
192
|
-
moduleDefinitions
|
|
193
|
-
).filter((field) => !coreCredentialFields.includes(field));
|
|
179
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
180
|
+
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
194
181
|
|
|
195
182
|
if (credentialFields.length === 0) {
|
|
196
183
|
return;
|
|
197
184
|
}
|
|
198
185
|
|
|
199
|
-
|
|
200
|
-
// than replacing it (registerCustomSchema overwrites).
|
|
201
|
-
const existingCredentialFields = customSchema.Credential?.fields || [];
|
|
202
|
-
const mergedSchema = {
|
|
203
|
-
...customSchema,
|
|
186
|
+
const moduleSchema = {
|
|
204
187
|
Credential: {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
...new Set([...existingCredentialFields, ...credentialFields]),
|
|
208
|
-
],
|
|
209
|
-
},
|
|
188
|
+
fields: credentialFields
|
|
189
|
+
}
|
|
210
190
|
};
|
|
211
191
|
|
|
212
192
|
logger.info(
|
|
213
193
|
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
214
194
|
);
|
|
215
195
|
|
|
216
|
-
registerCustomSchema(
|
|
196
|
+
registerCustomSchema(moduleSchema);
|
|
217
197
|
}
|
|
218
198
|
|
|
219
199
|
/**
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
|
+
const {
|
|
5
|
+
applyAppDefinitionDefaults,
|
|
6
|
+
} = require('@friggframework/schemas/app-definition');
|
|
4
7
|
const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
|
|
5
8
|
const { registerDeniedKeys } = require('../logs/denied-keys');
|
|
6
9
|
const {
|
|
@@ -33,7 +36,9 @@ function registerCredentialLogKeys(appDefinition, integrations) {
|
|
|
33
36
|
* @function loadAppDefinition
|
|
34
37
|
* @description Searches for the nearest backend package.json, loads the corresponding index.js file,
|
|
35
38
|
* and extracts the application definition containing integrations and user configuration.
|
|
36
|
-
*
|
|
39
|
+
* The defaults declared in the app-definition schema are applied first (ADR-051), so
|
|
40
|
+
* `userConfig` always holds a complete user config, also when the app omits `user`.
|
|
41
|
+
* @returns {{integrations: Array<object>, userConfig: object, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
|
|
37
42
|
* @throws {Error} Throws error if backend package.json cannot be found.
|
|
38
43
|
* @throws {Error} Throws error if index.js file cannot be found in the backend directory.
|
|
39
44
|
* @example
|
|
@@ -53,7 +58,9 @@ function loadAppDefinition() {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
const backendJsFile = require(backendFilePath);
|
|
56
|
-
|
|
61
|
+
// The one place schema defaults are applied at runtime (ADR-051): a
|
|
62
|
+
// default in app-definition.schema.json is the value core sees.
|
|
63
|
+
const appDefinition = applyAppDefinitionDefaults(backendJsFile.Definition);
|
|
57
64
|
|
|
58
65
|
const {
|
|
59
66
|
integrations = [],
|
|
@@ -164,38 +164,6 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
|
|
|
164
164
|
return this._mapIntegration(created);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
168
|
-
const objectId = toObjectId(integrationId);
|
|
169
|
-
if (!objectId) return null;
|
|
170
|
-
const result = await updateOne(
|
|
171
|
-
this.prisma,
|
|
172
|
-
'Integration',
|
|
173
|
-
{ _id: objectId },
|
|
174
|
-
{
|
|
175
|
-
$addToSet: { entityIds: { $each: toObjectIdArray(entityIds) } },
|
|
176
|
-
$set: { updatedAt: { $date: new Date().toISOString() } },
|
|
177
|
-
}
|
|
178
|
-
);
|
|
179
|
-
if (!result?.n) return null;
|
|
180
|
-
return this.findIntegrationById(integrationId);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
184
|
-
const objectId = toObjectId(integrationId);
|
|
185
|
-
if (!objectId) return null;
|
|
186
|
-
const result = await updateOne(
|
|
187
|
-
this.prisma,
|
|
188
|
-
'Integration',
|
|
189
|
-
{ _id: objectId },
|
|
190
|
-
{
|
|
191
|
-
$pullAll: { entityIds: toObjectIdArray(entityIds) },
|
|
192
|
-
$set: { updatedAt: { $date: new Date().toISOString() } },
|
|
193
|
-
}
|
|
194
|
-
);
|
|
195
|
-
if (!result?.n) return null;
|
|
196
|
-
return this.findIntegrationById(integrationId);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
167
|
async findIntegrationByUserId(userId) {
|
|
200
168
|
const objectId = toObjectId(userId);
|
|
201
169
|
if (!objectId) return null;
|
|
@@ -184,36 +184,6 @@ class IntegrationRepositoryInterface {
|
|
|
184
184
|
'Method findIntegrationsByEntityId must be implemented by subclass'
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Attach entities to an existing integration. Entities that are already
|
|
190
|
-
* attached are left as they are.
|
|
191
|
-
*
|
|
192
|
-
* @param {string|number} integrationId - Integration ID
|
|
193
|
-
* @param {Array<string|number>} entityIds - Entity IDs to attach
|
|
194
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
195
|
-
* @abstract
|
|
196
|
-
*/
|
|
197
|
-
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
'Method addEntitiesToIntegration must be implemented by subclass'
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Detach entities from an integration. Entities that are not attached
|
|
205
|
-
* are ignored; the entities themselves are not deleted.
|
|
206
|
-
*
|
|
207
|
-
* @param {string|number} integrationId - Integration ID
|
|
208
|
-
* @param {Array<string|number>} entityIds - Entity IDs to detach
|
|
209
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
210
|
-
* @abstract
|
|
211
|
-
*/
|
|
212
|
-
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
213
|
-
throw new Error(
|
|
214
|
-
'Method removeEntitiesFromIntegration must be implemented by subclass'
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
187
|
}
|
|
218
188
|
|
|
219
189
|
module.exports = { IntegrationRepositoryInterface };
|
|
@@ -338,58 +338,6 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
338
338
|
};
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
/**
|
|
342
|
-
* Attach entities to an existing integration. Entities already attached
|
|
343
|
-
* are left as they are.
|
|
344
|
-
*
|
|
345
|
-
* @param {string} integrationId - Integration ID
|
|
346
|
-
* @param {Array<string>} entityIds - Entity IDs to attach
|
|
347
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
348
|
-
*/
|
|
349
|
-
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
await this.prisma.integration.update({
|
|
353
|
-
where: { id: integrationId },
|
|
354
|
-
data: {
|
|
355
|
-
entities: {
|
|
356
|
-
connect: entityIds.map((id) => ({ id })),
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
} catch (error) {
|
|
361
|
-
if (error.code === 'P2025') return null;
|
|
362
|
-
throw error;
|
|
363
|
-
}
|
|
364
|
-
return this.findIntegrationById(integrationId);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Detach entities from an integration. Entities that are not attached
|
|
369
|
-
* are ignored. The entities themselves are not deleted.
|
|
370
|
-
*
|
|
371
|
-
* @param {string} integrationId - Integration ID
|
|
372
|
-
* @param {Array<string>} entityIds - Entity IDs to detach
|
|
373
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
374
|
-
*/
|
|
375
|
-
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
await this.prisma.integration.update({
|
|
379
|
-
where: { id: integrationId },
|
|
380
|
-
data: {
|
|
381
|
-
entities: {
|
|
382
|
-
disconnect: entityIds.map((id) => ({ id })),
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
});
|
|
386
|
-
} catch (error) {
|
|
387
|
-
if (error.code === 'P2025') return null;
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
return this.findIntegrationById(integrationId);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
341
|
/**
|
|
394
342
|
* Find integration by user ID (returns single integration)
|
|
395
343
|
* Replaces: IntegrationModel.findOne({ user: userId }).populate('entities')
|
|
@@ -365,64 +365,6 @@ class IntegrationRepositoryPostgres extends IntegrationRepositoryInterface {
|
|
|
365
365
|
};
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
/**
|
|
369
|
-
* Attach entities to an existing integration. Entities already attached
|
|
370
|
-
* are left as they are.
|
|
371
|
-
*
|
|
372
|
-
* @param {string} integrationId - Integration ID (string from application layer)
|
|
373
|
-
* @param {Array<string>} entityIds - Entity IDs to attach
|
|
374
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
375
|
-
*/
|
|
376
|
-
async addEntitiesToIntegration(integrationId, entityIds) {
|
|
377
|
-
const intId = this._convertId(integrationId);
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
await this.prisma.integration.update({
|
|
381
|
-
where: { id: intId },
|
|
382
|
-
data: {
|
|
383
|
-
entities: {
|
|
384
|
-
connect: entityIds.map((id) => ({
|
|
385
|
-
id: this._convertId(id),
|
|
386
|
-
})),
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
} catch (error) {
|
|
391
|
-
if (error.code === 'P2025') return null;
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
return this.findIntegrationById(integrationId);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Detach entities from an integration. Entities that are not attached
|
|
399
|
-
* are ignored. The entities themselves are not deleted.
|
|
400
|
-
*
|
|
401
|
-
* @param {string} integrationId - Integration ID (string from application layer)
|
|
402
|
-
* @param {Array<string>} entityIds - Entity IDs to detach
|
|
403
|
-
* @returns {Promise<Object|null>} Updated integration, or null when the integration does not exist
|
|
404
|
-
*/
|
|
405
|
-
async removeEntitiesFromIntegration(integrationId, entityIds) {
|
|
406
|
-
const intId = this._convertId(integrationId);
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
await this.prisma.integration.update({
|
|
410
|
-
where: { id: intId },
|
|
411
|
-
data: {
|
|
412
|
-
entities: {
|
|
413
|
-
disconnect: entityIds.map((id) => ({
|
|
414
|
-
id: this._convertId(id),
|
|
415
|
-
})),
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
} catch (error) {
|
|
420
|
-
if (error.code === 'P2025') return null;
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
return this.findIntegrationById(integrationId);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
368
|
/**
|
|
427
369
|
* Find integration by user ID (returns single integration)
|
|
428
370
|
*
|
|
@@ -62,7 +62,7 @@ class CreateIntegration {
|
|
|
62
62
|
|
|
63
63
|
// A concurrent request may have created a matching row between the
|
|
64
64
|
// lookup above and this insert; re-check before any side effects run.
|
|
65
|
-
const survivor = await this.
|
|
65
|
+
const survivor = await this._findDuplicate(
|
|
66
66
|
userId,
|
|
67
67
|
config?.type,
|
|
68
68
|
entities
|
|
@@ -135,68 +135,30 @@ class CreateIntegration {
|
|
|
135
135
|
return mapIntegrationClassToIntegrationDTO(integrationInstance);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
async
|
|
138
|
+
async _findDuplicate(userId, type, entityIds) {
|
|
139
139
|
const candidates =
|
|
140
140
|
await this.integrationRepository.findIntegrationsByUserId(userId);
|
|
141
|
-
const target =
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const entitySet = (integration) =>
|
|
157
|
-
new Set((integration.entitiesIds ?? []).map(String));
|
|
158
|
-
|
|
159
|
-
const exact = live.filter((integration) => {
|
|
160
|
-
const current = entitySet(integration);
|
|
141
|
+
const target = [...(entityIds ?? [])].map(String).sort();
|
|
142
|
+
|
|
143
|
+
const matches = candidates.filter((integration) => {
|
|
144
|
+
// An IN_DELETION row is either mid-teardown or a zombie left
|
|
145
|
+
// behind by a failed deleteIntegrationById call — never a live
|
|
146
|
+
// integration. Treating it as a duplicate would hand a reinstall
|
|
147
|
+
// attempt a row nothing will ever move out of IN_DELETION,
|
|
148
|
+
// silently discarded by the queue worker forever. Let a fresh
|
|
149
|
+
// create proceed instead; the zombie is cleaned up out-of-band,
|
|
150
|
+
// matching how teardown failures are already handled here.
|
|
151
|
+
if (integration.status === 'IN_DELETION') return false;
|
|
152
|
+
if (integration.config?.type !== type) return false;
|
|
153
|
+
const current = [...(integration.entitiesIds ?? [])]
|
|
154
|
+
.map(String)
|
|
155
|
+
.sort();
|
|
161
156
|
return (
|
|
162
|
-
current.
|
|
163
|
-
|
|
157
|
+
current.length === target.length &&
|
|
158
|
+
current.every((id, index) => id === target[index])
|
|
164
159
|
);
|
|
165
160
|
});
|
|
166
161
|
|
|
167
|
-
// Entities may have been attached after create (addEntitiesToIntegration),
|
|
168
|
-
// so a reconnect that sends the original entities must still find the
|
|
169
|
-
// integration that now holds them, instead of creating a second one
|
|
170
|
-
// that would share those entities.
|
|
171
|
-
const supersets = live.filter((integration) => {
|
|
172
|
-
const current = entitySet(integration);
|
|
173
|
-
return [...target].every((id) => current.has(id));
|
|
174
|
-
});
|
|
175
|
-
return { exact, supersets: target.size ? supersets : [] };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async _findDuplicate(userId, type, entityIds) {
|
|
179
|
-
const { exact, supersets } = await this._findMatches(
|
|
180
|
-
userId,
|
|
181
|
-
type,
|
|
182
|
-
entityIds
|
|
183
|
-
);
|
|
184
|
-
if (exact.length) return this._pickOldest(exact);
|
|
185
|
-
return supersets.length ? this._pickOldest(supersets) : null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// After an insert the new row is itself an exact match, so the survivor
|
|
189
|
-
// is the oldest of every exact or superset match (the new row included):
|
|
190
|
-
// an older integration that already holds these entities always wins.
|
|
191
|
-
async _findSurvivor(userId, type, entityIds) {
|
|
192
|
-
const { exact, supersets } = await this._findMatches(
|
|
193
|
-
userId,
|
|
194
|
-
type,
|
|
195
|
-
entityIds
|
|
196
|
-
);
|
|
197
|
-
// supersets includes the exact matches; it is empty only for an
|
|
198
|
-
// empty entity set, where exact matching alone applies.
|
|
199
|
-
const matches = supersets.length ? supersets : exact;
|
|
200
162
|
return matches.length ? this._pickOldest(matches) : null;
|
|
201
163
|
}
|
|
202
164
|
|
package/jest-setup.js
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// MongoDB (as @friggframework/test's globalSetup does): nothing in the default
|
|
4
|
-
// suite talks to a live server, and a binary that failed to start used to stop
|
|
5
|
-
// every core suite from running. Suites that need a real database are
|
|
6
|
-
// `*.db.test.js` files, run by `npm run test:db` (see jest.db.config.js).
|
|
7
|
-
const { overrideEnvironment } = require('@friggframework/test');
|
|
8
|
-
|
|
9
|
-
module.exports = async function () {
|
|
10
|
-
if (!process.env.STAGE) {
|
|
11
|
-
overrideEnvironment({ STAGE: 'dev' });
|
|
12
|
-
}
|
|
13
|
-
};
|
|
1
|
+
const {globalSetup} = require('@friggframework/test');
|
|
2
|
+
module.exports = globalSetup;
|
package/jest-teardown.js
CHANGED
package/jest.config.js
CHANGED
|
@@ -20,8 +20,4 @@ module.exports = {
|
|
|
20
20
|
globalTeardown: './jest-teardown.js',
|
|
21
21
|
|
|
22
22
|
setupFilesAfterEnv: ['./logs/jest-logger-setup.js'],
|
|
23
|
-
|
|
24
|
-
// `*.db.test.js` suites talk to a real MongoDB replica set / PostgreSQL
|
|
25
|
-
// and run separately via `npm run test:db` (jest.db.config.js).
|
|
26
|
-
testPathIgnorePatterns: ['/node_modules/', '\\.db\\.test\\.js$'],
|
|
27
23
|
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const { toCredentialId } = require('./to-credential-id');
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {import('../module').Module} moduleInstance
|
|
5
3
|
* Convert a Module domain instance to a plain DTO suitable for JSON responses.
|
|
@@ -12,9 +10,9 @@ function mapModuleClassToModuleDTO(moduleInstance) {
|
|
|
12
10
|
name: moduleInstance.name,
|
|
13
11
|
userId: moduleInstance.userId,
|
|
14
12
|
entity: moduleInstance.entity,
|
|
15
|
-
credentialId:
|
|
16
|
-
type: moduleInstance.getName()
|
|
13
|
+
credentialId: moduleInstance.credential?._id?.toString(),
|
|
14
|
+
type: moduleInstance.getName()
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
module.exports = { mapModuleClassToModuleDTO };
|
|
18
|
+
module.exports = { mapModuleClassToModuleDTO };
|
package/package.json
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
|
+
"description": "",
|
|
3
4
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
5
|
+
"version": "2.0.0--canary.702.ef0dc8c.0",
|
|
5
6
|
"dependencies": {
|
|
6
7
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
8
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
8
9
|
"@aws-sdk/client-lambda": "^3.714.0",
|
|
9
10
|
"@aws-sdk/client-s3": "^3.588.0",
|
|
10
|
-
"@aws-sdk/client-scheduler": "^3.
|
|
11
|
+
"@aws-sdk/client-scheduler": "^3.1153.0",
|
|
11
12
|
"@aws-sdk/client-sqs": "^3.588.0",
|
|
12
13
|
"@aws-sdk/client-ssm": "^3.588.0",
|
|
13
14
|
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
|
15
|
+
"@friggframework/schemas": "2.0.0--canary.702.ef0dc8c.0",
|
|
14
16
|
"@hapi/boom": "^10.0.1",
|
|
15
17
|
"@opentelemetry/api": "^1.9.1",
|
|
16
18
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -48,9 +50,9 @@
|
|
|
48
50
|
}
|
|
49
51
|
},
|
|
50
52
|
"devDependencies": {
|
|
51
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
53
|
-
"@friggframework/test": "2.0.0--canary.
|
|
53
|
+
"@friggframework/eslint-config": "2.0.0--canary.702.ef0dc8c.0",
|
|
54
|
+
"@friggframework/prettier-config": "2.0.0--canary.702.ef0dc8c.0",
|
|
55
|
+
"@friggframework/test": "2.0.0--canary.702.ef0dc8c.0",
|
|
54
56
|
"@prisma/client": "^6.19.3",
|
|
55
57
|
"@types/lodash": "4.17.15",
|
|
56
58
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -68,7 +70,6 @@
|
|
|
68
70
|
"scripts": {
|
|
69
71
|
"lint:fix": "prettier --write --loglevel error . && eslint . --fix",
|
|
70
72
|
"test": "jest --passWithNoTests # TODO",
|
|
71
|
-
"test:db": "jest --config jest.db.config.js --runInBand",
|
|
72
73
|
"prisma:generate:mongo": "npx prisma generate --schema ./prisma-mongodb/schema.prisma",
|
|
73
74
|
"prisma:generate:postgres": "npx prisma generate --schema ./prisma-postgresql/schema.prisma",
|
|
74
75
|
"prisma:generate": "npm run prisma:generate:mongo && npm run prisma:generate:postgres",
|
|
@@ -87,9 +88,8 @@
|
|
|
87
88
|
"url": "https://github.com/friggframework/frigg/issues"
|
|
88
89
|
},
|
|
89
90
|
"homepage": "https://github.com/friggframework/frigg#readme",
|
|
90
|
-
"description": "",
|
|
91
91
|
"publishConfig": {
|
|
92
92
|
"access": "public"
|
|
93
93
|
},
|
|
94
|
-
"gitHead": "
|
|
94
|
+
"gitHead": "ef0dc8cb6c6f56048a61b6759faad7322046925d"
|
|
95
95
|
}
|
package/queues/queuer-util.js
CHANGED
|
@@ -97,17 +97,16 @@ const QueuerUtil = {
|
|
|
97
97
|
Id: uuid(),
|
|
98
98
|
MessageBody: JSON.stringify(entry),
|
|
99
99
|
});
|
|
100
|
-
// Sends 10, then
|
|
101
|
-
// array: clearing the shared buffer in place would also empty the
|
|
102
|
-
// Entries of a command that was already sent.
|
|
100
|
+
// Sends 10, then purges the buffer
|
|
103
101
|
if (buffer.length === batchSize) {
|
|
104
|
-
const batch = buffer.splice(0, buffer.length);
|
|
105
102
|
const command = new SendMessageBatchCommand({
|
|
106
|
-
Entries:
|
|
103
|
+
Entries: buffer,
|
|
107
104
|
QueueUrl: queueUrl,
|
|
108
105
|
});
|
|
109
106
|
const result = await sqs.send(command);
|
|
110
|
-
inspectBatchResult(result, queueUrl,
|
|
107
|
+
inspectBatchResult(result, queueUrl, buffer);
|
|
108
|
+
// Purge the buffer
|
|
109
|
+
buffer.splice(0, buffer.length);
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -23,7 +23,7 @@ class TestUserRepository {
|
|
|
23
23
|
|
|
24
24
|
async createToken(userId, rawToken, minutes = 120) {
|
|
25
25
|
const token = `token-for-${userId}-for-${minutes}-mins`;
|
|
26
|
-
this.tokens.set(token, {
|
|
26
|
+
this.tokens.set(token, { user: userId, rawToken });
|
|
27
27
|
return token;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -40,13 +40,8 @@ class GetUserFromBearerToken {
|
|
|
40
40
|
throw Boom.unauthorized('Session Token Not Found');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const tokenUserId = sessionToken.userId;
|
|
44
|
-
if (!tokenUserId) {
|
|
45
|
-
throw Boom.unauthorized('Session Token Has No User');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
if (this.userConfig.primary === 'organization') {
|
|
49
|
-
const organizationUserData = await this.userRepository.findOrganizationUserById(
|
|
44
|
+
const organizationUserData = await this.userRepository.findOrganizationUserById(sessionToken.user);
|
|
50
45
|
|
|
51
46
|
if (!organizationUserData) {
|
|
52
47
|
throw Boom.unauthorized('Organization User Not Found');
|
|
@@ -62,7 +57,7 @@ class GetUserFromBearerToken {
|
|
|
62
57
|
);
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
const individualUserData = await this.userRepository.findIndividualUserById(
|
|
60
|
+
const individualUserData = await this.userRepository.findIndividualUserById(sessionToken.user);
|
|
66
61
|
|
|
67
62
|
if (!individualUserData) {
|
|
68
63
|
throw Boom.unauthorized('Individual User Not Found');
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
assertEntityIdsInput,
|
|
3
|
-
integrationNotFound,
|
|
4
|
-
loadIntegration,
|
|
5
|
-
} = require('../utils/assert-entity-ids-input');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Attaches entities to an existing integration, e.g. when a user grants
|
|
9
|
-
* access to more accounts after the integration was created. Every entity
|
|
10
|
-
* must exist, belong to the integration's user, and be of a module the
|
|
11
|
-
* integration already holds (so hydration can load it). Entities that are
|
|
12
|
-
* already attached are left as they are.
|
|
13
|
-
* @class AddEntitiesToIntegration
|
|
14
|
-
*/
|
|
15
|
-
class AddEntitiesToIntegration {
|
|
16
|
-
/**
|
|
17
|
-
* @param {Object} params
|
|
18
|
-
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
|
|
19
|
-
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
20
|
-
*/
|
|
21
|
-
constructor({ integrationRepository, moduleRepository }) {
|
|
22
|
-
this.integrationRepository = integrationRepository;
|
|
23
|
-
this.moduleRepository = moduleRepository;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {string} integrationId
|
|
28
|
-
* @param {string[]} entityIds
|
|
29
|
-
* @returns {Promise<Object>} The updated integration record.
|
|
30
|
-
*/
|
|
31
|
-
async execute(integrationId, entityIds) {
|
|
32
|
-
assertEntityIdsInput(integrationId, entityIds);
|
|
33
|
-
|
|
34
|
-
const integration = await loadIntegration(
|
|
35
|
-
this.integrationRepository,
|
|
36
|
-
integrationId
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const entities = await this.moduleRepository.findEntitiesByIds(
|
|
40
|
-
entityIds
|
|
41
|
-
);
|
|
42
|
-
const foundIds = new Set(entities.map((e) => String(e.id)));
|
|
43
|
-
const missing = entityIds.filter((id) => !foundIds.has(String(id)));
|
|
44
|
-
if (missing.length > 0) {
|
|
45
|
-
const error = new Error(
|
|
46
|
-
`Entities not found: ${missing.join(', ')}`
|
|
47
|
-
);
|
|
48
|
-
error.code = 'ENTITIES_NOT_FOUND';
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const foreign = entities.filter(
|
|
53
|
-
(e) => String(e.userId) !== String(integration.userId)
|
|
54
|
-
);
|
|
55
|
-
if (foreign.length > 0) {
|
|
56
|
-
const error = new Error(
|
|
57
|
-
`Entities do not belong to the integration's user: ${foreign
|
|
58
|
-
.map((e) => e.id)
|
|
59
|
-
.join(', ')}`
|
|
60
|
-
);
|
|
61
|
-
error.code = 'ENTITY_USER_MISMATCH';
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const attached = await this.moduleRepository.findEntitiesByIds(
|
|
66
|
-
integration.entitiesIds ?? []
|
|
67
|
-
);
|
|
68
|
-
const heldModules = new Set(attached.map((e) => e.moduleName));
|
|
69
|
-
const otherModules = entities.filter(
|
|
70
|
-
(e) => !heldModules.has(e.moduleName)
|
|
71
|
-
);
|
|
72
|
-
if (otherModules.length > 0) {
|
|
73
|
-
const error = new Error(
|
|
74
|
-
`Entities belong to a module the integration does not hold: ${otherModules
|
|
75
|
-
.map((e) => `${e.id} (${e.moduleName})`)
|
|
76
|
-
.join(', ')}`
|
|
77
|
-
);
|
|
78
|
-
error.code = 'ENTITY_MODULE_MISMATCH';
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const updated =
|
|
83
|
-
await this.integrationRepository.addEntitiesToIntegration(
|
|
84
|
-
integrationId,
|
|
85
|
-
entityIds
|
|
86
|
-
);
|
|
87
|
-
if (!updated) throw integrationNotFound(integrationId);
|
|
88
|
-
return updated;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { AddEntitiesToIntegration };
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
assertEntityIdsInput,
|
|
3
|
-
integrationNotFound,
|
|
4
|
-
loadIntegration,
|
|
5
|
-
} = require('../utils/assert-entity-ids-input');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Detaches entities from an integration. Entities that are not attached are
|
|
9
|
-
* ignored, and detached entities are not deleted. Every module the
|
|
10
|
-
* integration holds must keep at least one entity, so the integration can
|
|
11
|
-
* still hydrate each of its modules.
|
|
12
|
-
* @class RemoveEntitiesFromIntegration
|
|
13
|
-
*/
|
|
14
|
-
class RemoveEntitiesFromIntegration {
|
|
15
|
-
/**
|
|
16
|
-
* @param {Object} params
|
|
17
|
-
* @param {import('../repositories/integration-repository-interface').IntegrationRepositoryInterface} params.integrationRepository
|
|
18
|
-
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
19
|
-
*/
|
|
20
|
-
constructor({ integrationRepository, moduleRepository }) {
|
|
21
|
-
this.integrationRepository = integrationRepository;
|
|
22
|
-
this.moduleRepository = moduleRepository;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {string} integrationId
|
|
27
|
-
* @param {string[]} entityIds
|
|
28
|
-
* @returns {Promise<Object>} The updated integration record.
|
|
29
|
-
*/
|
|
30
|
-
async execute(integrationId, entityIds) {
|
|
31
|
-
assertEntityIdsInput(integrationId, entityIds);
|
|
32
|
-
|
|
33
|
-
const integration = await loadIntegration(
|
|
34
|
-
this.integrationRepository,
|
|
35
|
-
integrationId
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const removing = new Set(entityIds.map(String));
|
|
39
|
-
const attached = await this.moduleRepository.findEntitiesByIds(
|
|
40
|
-
integration.entitiesIds ?? []
|
|
41
|
-
);
|
|
42
|
-
const emptied = [...new Set(attached.map((e) => e.moduleName))].filter(
|
|
43
|
-
(moduleName) =>
|
|
44
|
-
!attached.some(
|
|
45
|
-
(e) =>
|
|
46
|
-
e.moduleName === moduleName &&
|
|
47
|
-
!removing.has(String(e.id))
|
|
48
|
-
)
|
|
49
|
-
);
|
|
50
|
-
if (emptied.length > 0) {
|
|
51
|
-
const error = new Error(
|
|
52
|
-
`Removing these entities would leave no entity for module(s): ${emptied.join(
|
|
53
|
-
', '
|
|
54
|
-
)}`
|
|
55
|
-
);
|
|
56
|
-
error.code = 'LAST_ENTITY_OF_MODULE';
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const updated =
|
|
61
|
-
await this.integrationRepository.removeEntitiesFromIntegration(
|
|
62
|
-
integrationId,
|
|
63
|
-
entityIds
|
|
64
|
-
);
|
|
65
|
-
if (!updated) throw integrationNotFound(integrationId);
|
|
66
|
-
return updated;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
module.exports = { RemoveEntitiesFromIntegration };
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Throws INVALID_INTEGRATION_DATA unless an integration id and a non-empty
|
|
3
|
-
* list of entity ids are given.
|
|
4
|
-
* @param {string} integrationId
|
|
5
|
-
* @param {string[]} entityIds
|
|
6
|
-
*/
|
|
7
|
-
function assertEntityIdsInput(integrationId, entityIds) {
|
|
8
|
-
if (!integrationId || !Array.isArray(entityIds) || entityIds.length === 0) {
|
|
9
|
-
const error = new Error(
|
|
10
|
-
'integrationId and a non-empty entityIds array are required'
|
|
11
|
-
);
|
|
12
|
-
error.code = 'INVALID_INTEGRATION_DATA';
|
|
13
|
-
throw error;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function integrationNotFound(integrationId) {
|
|
18
|
-
const error = new Error(`Integration ${integrationId} not found`);
|
|
19
|
-
error.code = 'INTEGRATION_NOT_FOUND';
|
|
20
|
-
return error;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Loads an integration, mapping the repositories' "not found" error to
|
|
25
|
-
* INTEGRATION_NOT_FOUND. Any other failure (connection, invalid id) is
|
|
26
|
-
* rethrown unchanged.
|
|
27
|
-
* @param {Object} integrationRepository
|
|
28
|
-
* @param {string} integrationId
|
|
29
|
-
* @returns {Promise<Object>} The integration record.
|
|
30
|
-
*/
|
|
31
|
-
async function loadIntegration(integrationRepository, integrationId) {
|
|
32
|
-
try {
|
|
33
|
-
return await integrationRepository.findIntegrationById(integrationId);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (/^Integration with id .* not found$/.test(error?.message)) {
|
|
36
|
-
throw integrationNotFound(integrationId);
|
|
37
|
-
}
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
module.exports = { assertEntityIdsInput, integrationNotFound, loadIntegration };
|
package/jest.db.config.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Database integration suites (`*.db.test.js`).
|
|
3
|
-
*
|
|
4
|
-
* These talk to real databases instead of mocking Prisma, so they are kept
|
|
5
|
-
* out of the default `npm test` run and executed with `npm run test:db`.
|
|
6
|
-
* They need generated Prisma clients (`npm run prisma:generate`) and:
|
|
7
|
-
* - a MongoDB replica set (Prisma requires one for MongoDB writes):
|
|
8
|
-
* FRIGG_TEST_MONGODB_URL, default
|
|
9
|
-
* mongodb://localhost:27017/frigg?replicaSet=rs0
|
|
10
|
-
* - PostgreSQL with the prisma-postgresql migrations applied:
|
|
11
|
-
* FRIGG_TEST_POSTGRES_URL, default
|
|
12
|
-
* postgresql://postgres:postgres@localhost:5432/frigg?schema=public
|
|
13
|
-
* The repository parity suites skip themselves unless their own URLs are
|
|
14
|
-
* set (INTEGRATION_ENTITIES_PARITY_* / QUERY_MAPPINGS_PARITY_*).
|
|
15
|
-
*/
|
|
16
|
-
const base = require('./jest.config');
|
|
17
|
-
|
|
18
|
-
module.exports = {
|
|
19
|
-
...base,
|
|
20
|
-
coverageThreshold: undefined,
|
|
21
|
-
testMatch: ['**/*.db.test.js'],
|
|
22
|
-
testPathIgnorePatterns: ['/node_modules/'],
|
|
23
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve the credential id from an entity's or module's `credential`, which
|
|
3
|
-
* repositories return as the decrypted credential object (or a bare id).
|
|
4
|
-
* @param {Object|string|number|null|undefined} credential
|
|
5
|
-
* @returns {string|null}
|
|
6
|
-
*/
|
|
7
|
-
function toCredentialId(credential) {
|
|
8
|
-
if (credential && typeof credential === 'object') {
|
|
9
|
-
return credential.id?.toString() ?? null;
|
|
10
|
-
}
|
|
11
|
-
return credential?.toString() ?? null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
module.exports = { toCredentialId };
|