@friggframework/core 2.0.0--canary.667.f74cf3b.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/database/documentdb-utils.js +4 -1
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +9 -2
- package/index.js +2 -0
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +22 -18
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-mongo.js +1 -1
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/modules/use-cases/get-entity-options-by-id.js +5 -7
- package/modules/use-cases/get-module.js +8 -29
- package/modules/use-cases/refresh-entity-options.js +5 -7
- package/modules/use-cases/test-module-auth.js +5 -7
- package/package.json +8 -6
- package/modules/use-cases/create-entity-from-credential.js +0 -105
- package/modules/utils/find-module-definition.js +0 -23
|
@@ -58,7 +58,10 @@ async function insertOne(client, collection, document) {
|
|
|
58
58
|
|
|
59
59
|
// Provide helpful context for common errors
|
|
60
60
|
if (error.code === 11000) {
|
|
61
|
-
throw
|
|
61
|
+
throw Object.assign(
|
|
62
|
+
new Error(`${errorMsg} - Duplicate key violation`),
|
|
63
|
+
{ code: 11000 }
|
|
64
|
+
);
|
|
62
65
|
}
|
|
63
66
|
throw new Error(errorMsg);
|
|
64
67
|
}
|
package/errors/index.js
CHANGED
|
@@ -6,6 +6,9 @@ const {
|
|
|
6
6
|
ParameterTypeError,
|
|
7
7
|
} = require('./validation-errors');
|
|
8
8
|
const { ClientSafeError } = require('./client-safe-error');
|
|
9
|
+
const {
|
|
10
|
+
MappingAlreadyExistsError,
|
|
11
|
+
} = require('./mapping-already-exists-error');
|
|
9
12
|
|
|
10
13
|
module.exports = {
|
|
11
14
|
BaseError,
|
|
@@ -14,4 +17,5 @@ module.exports = {
|
|
|
14
17
|
RequiredPropertyError,
|
|
15
18
|
ParameterTypeError,
|
|
16
19
|
ClientSafeError,
|
|
20
|
+
MappingAlreadyExistsError,
|
|
17
21
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { BaseError } = require('./base-error');
|
|
2
|
+
|
|
3
|
+
class MappingAlreadyExistsError extends BaseError {
|
|
4
|
+
constructor(integrationId, sourceId, ...errorOptions) {
|
|
5
|
+
super(
|
|
6
|
+
`Mapping already exists for integration ${integrationId} and sourceId ${sourceId}`,
|
|
7
|
+
...errorOptions
|
|
8
|
+
);
|
|
9
|
+
this.integrationId = integrationId;
|
|
10
|
+
this.sourceId = sourceId;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { MappingAlreadyExistsError };
|
package/handlers/WEBHOOKS.md
CHANGED
|
@@ -483,6 +483,35 @@ async onWebhook({ data }) {
|
|
|
483
483
|
}
|
|
484
484
|
```
|
|
485
485
|
|
|
486
|
+
`getMapping` followed by `upsertMapping` is not atomic. To claim an event so only one
|
|
487
|
+
concurrent worker proceeds, use the insert-only `createMapping`, which throws
|
|
488
|
+
`MappingAlreadyExistsError` (exported from `@friggframework/core`) when the
|
|
489
|
+
`sourceId` is already taken. `deleteMapping(sourceId)` releases the claim and is a
|
|
490
|
+
no-op when nothing exists.
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
const { MappingAlreadyExistsError } = require('@friggframework/core');
|
|
494
|
+
|
|
495
|
+
async onWebhook({ data }) {
|
|
496
|
+
const claimId = `claim:${data.body.id}`;
|
|
497
|
+
try {
|
|
498
|
+
await this.createMapping(claimId, { claimedAt: new Date() });
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof MappingAlreadyExistsError) {
|
|
501
|
+
return { processed: false, duplicate: true };
|
|
502
|
+
}
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await this.processEvent(data.body);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
await this.deleteMapping(claimId);
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
486
515
|
### 4. Error Handling
|
|
487
516
|
```javascript
|
|
488
517
|
async onWebhook({ data }) {
|
|
@@ -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 = [],
|
package/index.js
CHANGED
|
@@ -57,6 +57,7 @@ const {
|
|
|
57
57
|
HaltError,
|
|
58
58
|
RequiredPropertyError,
|
|
59
59
|
ParameterTypeError,
|
|
60
|
+
MappingAlreadyExistsError,
|
|
60
61
|
} = require('./errors/index');
|
|
61
62
|
const {
|
|
62
63
|
IntegrationBase,
|
|
@@ -144,6 +145,7 @@ module.exports = {
|
|
|
144
145
|
HaltError,
|
|
145
146
|
RequiredPropertyError,
|
|
146
147
|
ParameterTypeError,
|
|
148
|
+
MappingAlreadyExistsError,
|
|
147
149
|
|
|
148
150
|
// integrations
|
|
149
151
|
IntegrationBase,
|
|
@@ -11,19 +11,65 @@
|
|
|
11
11
|
* This adapter implements SchedulerServiceInterface for AWS EventBridge Scheduler.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const {
|
|
15
|
-
SchedulerClient,
|
|
16
|
-
CreateScheduleCommand,
|
|
17
|
-
DeleteScheduleCommand,
|
|
18
|
-
GetScheduleCommand,
|
|
19
|
-
ResourceNotFoundException,
|
|
20
|
-
} = require('@aws-sdk/client-scheduler');
|
|
21
|
-
|
|
22
14
|
const { SchedulerServiceInterface } = require('./scheduler-service-interface');
|
|
23
15
|
|
|
16
|
+
const SCHEDULER_SDK = '@aws-sdk/client-scheduler';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lazily load the AWS EventBridge Scheduler SDK.
|
|
20
|
+
*
|
|
21
|
+
* Requiring `@aws-sdk/client-scheduler` at module top level would put the AWS
|
|
22
|
+
* SDK on the load path of every consumer of `@friggframework/core`, including
|
|
23
|
+
* those using the mock scheduler or no scheduler at all. Loading it here, on
|
|
24
|
+
* construction of the EventBridge adapter, keeps it out of the module-load
|
|
25
|
+
* path. The adapter calls it exactly once, in its constructor, and keeps the
|
|
26
|
+
* classes its methods need on the instance.
|
|
27
|
+
*
|
|
28
|
+
* Only a MODULE_NOT_FOUND for the scheduler SDK itself is rewritten into an
|
|
29
|
+
* actionable message; any other failure (including a missing transitive
|
|
30
|
+
* dependency) is rethrown unchanged so the real cause is not masked.
|
|
31
|
+
*
|
|
32
|
+
* @returns {object} The relevant exports from `@aws-sdk/client-scheduler`.
|
|
33
|
+
*/
|
|
34
|
+
function loadSchedulerSdk() {
|
|
35
|
+
try {
|
|
36
|
+
// Keep the literal specifier so bundlers (esbuild) can resolve it.
|
|
37
|
+
return require('@aws-sdk/client-scheduler');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const isSchedulerSdkMissing =
|
|
40
|
+
error &&
|
|
41
|
+
error.code === 'MODULE_NOT_FOUND' &&
|
|
42
|
+
typeof error.message === 'string' &&
|
|
43
|
+
error.message.includes(SCHEDULER_SDK);
|
|
44
|
+
if (isSchedulerSdkMissing) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`The EventBridge scheduler requires the "${SCHEDULER_SDK}" package, ` +
|
|
47
|
+
`which is not installed. Install it (\`npm install ${SCHEDULER_SDK}\`) ` +
|
|
48
|
+
'to use the EventBridge scheduler provider, or use the mock scheduler provider ' +
|
|
49
|
+
'(SCHEDULER_PROVIDER=mock) for local development.',
|
|
50
|
+
{ cause: error }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
25
58
|
constructor({ region } = {}) {
|
|
26
59
|
super();
|
|
60
|
+
const {
|
|
61
|
+
SchedulerClient,
|
|
62
|
+
CreateScheduleCommand,
|
|
63
|
+
DeleteScheduleCommand,
|
|
64
|
+
GetScheduleCommand,
|
|
65
|
+
ResourceNotFoundException,
|
|
66
|
+
} = loadSchedulerSdk();
|
|
67
|
+
this.sdk = {
|
|
68
|
+
CreateScheduleCommand,
|
|
69
|
+
DeleteScheduleCommand,
|
|
70
|
+
GetScheduleCommand,
|
|
71
|
+
ResourceNotFoundException,
|
|
72
|
+
};
|
|
27
73
|
this.client = new SchedulerClient({
|
|
28
74
|
region: region || process.env.AWS_REGION || 'us-east-1',
|
|
29
75
|
});
|
|
@@ -42,7 +88,12 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
|
42
88
|
* @param {Object} params.payload - Message payload
|
|
43
89
|
* @returns {Promise<{scheduledJobId: string, scheduledAt: string}>}
|
|
44
90
|
*/
|
|
45
|
-
async scheduleOneTime({
|
|
91
|
+
async scheduleOneTime({
|
|
92
|
+
scheduleName,
|
|
93
|
+
scheduleAt,
|
|
94
|
+
queueResourceId,
|
|
95
|
+
payload,
|
|
96
|
+
}) {
|
|
46
97
|
if (!scheduleName) {
|
|
47
98
|
throw new Error('scheduleName is required');
|
|
48
99
|
}
|
|
@@ -59,8 +110,11 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
|
59
110
|
}
|
|
60
111
|
|
|
61
112
|
// Format date to AWS schedule expression (at(yyyy-mm-ddThh:mm:ss))
|
|
62
|
-
const scheduleExpression = `at(${scheduleAt
|
|
113
|
+
const scheduleExpression = `at(${scheduleAt
|
|
114
|
+
.toISOString()
|
|
115
|
+
.replace(/\.\d{3}Z$/, '')})`;
|
|
63
116
|
|
|
117
|
+
const { CreateScheduleCommand } = this.sdk;
|
|
64
118
|
const command = new CreateScheduleCommand({
|
|
65
119
|
Name: scheduleName,
|
|
66
120
|
GroupName: this.scheduleGroupName,
|
|
@@ -107,6 +161,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
|
107
161
|
throw new Error('scheduleName is required');
|
|
108
162
|
}
|
|
109
163
|
|
|
164
|
+
const { DeleteScheduleCommand, ResourceNotFoundException } = this.sdk;
|
|
110
165
|
const command = new DeleteScheduleCommand({
|
|
111
166
|
Name: scheduleName,
|
|
112
167
|
GroupName: this.scheduleGroupName,
|
|
@@ -141,6 +196,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
|
141
196
|
throw new Error('scheduleName is required');
|
|
142
197
|
}
|
|
143
198
|
|
|
199
|
+
const { GetScheduleCommand, ResourceNotFoundException } = this.sdk;
|
|
144
200
|
const command = new GetScheduleCommand({
|
|
145
201
|
Name: scheduleName,
|
|
146
202
|
GroupName: this.scheduleGroupName,
|
|
@@ -153,9 +209,7 @@ class EventBridgeSchedulerAdapter extends SchedulerServiceInterface {
|
|
|
153
209
|
// Format: at(yyyy-mm-ddThh:mm:ss)
|
|
154
210
|
let scheduledAt = null;
|
|
155
211
|
if (response.ScheduleExpression) {
|
|
156
|
-
const match = response.ScheduleExpression.match(
|
|
157
|
-
/^at\((.+)\)$/
|
|
158
|
-
);
|
|
212
|
+
const match = response.ScheduleExpression.match(/^at\((.+)\)$/);
|
|
159
213
|
if (match) {
|
|
160
214
|
scheduledAt = new Date(match[1] + 'Z').toISOString();
|
|
161
215
|
}
|
|
@@ -457,6 +457,35 @@ class IntegrationBase {
|
|
|
457
457
|
);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Insert-only mapping write; usable as an atomic claim/lock row.
|
|
462
|
+
* @throws {MappingAlreadyExistsError} If a mapping for sourceId exists
|
|
463
|
+
*/
|
|
464
|
+
async createMapping(sourceId, mapping) {
|
|
465
|
+
if (!sourceId) {
|
|
466
|
+
throw new Error(`sourceId must be set`);
|
|
467
|
+
}
|
|
468
|
+
return await this.integrationMappingRepository.createMapping(
|
|
469
|
+
this.id,
|
|
470
|
+
sourceId,
|
|
471
|
+
mapping
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Deletes the mapping for sourceId. A missing mapping is not an error
|
|
477
|
+
* (resolves with deletedCount 0).
|
|
478
|
+
*/
|
|
479
|
+
async deleteMapping(sourceId) {
|
|
480
|
+
if (!sourceId) {
|
|
481
|
+
throw new Error(`sourceId must be set`);
|
|
482
|
+
}
|
|
483
|
+
return await this.integrationMappingRepository.deleteMapping(
|
|
484
|
+
this.id,
|
|
485
|
+
sourceId
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
460
489
|
/**
|
|
461
490
|
* CHILDREN CAN OVERRIDE THESE CONFIGURATION METHODS
|
|
462
491
|
*/
|
|
@@ -42,9 +42,6 @@ const {
|
|
|
42
42
|
} = require('../modules/use-cases/get-entity-options-by-type');
|
|
43
43
|
const { TestModuleAuth } = require('../modules/use-cases/test-module-auth');
|
|
44
44
|
const { GetModule } = require('../modules/use-cases/get-module');
|
|
45
|
-
const {
|
|
46
|
-
CreateEntityFromCredential,
|
|
47
|
-
} = require('../modules/use-cases/create-entity-from-credential');
|
|
48
45
|
const {
|
|
49
46
|
GetEntityOptionsById,
|
|
50
47
|
} = require('../modules/use-cases/get-entity-options-by-id');
|
|
@@ -201,13 +198,6 @@ function createIntegrationRouter() {
|
|
|
201
198
|
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
202
199
|
});
|
|
203
200
|
|
|
204
|
-
const createEntityFromCredential = new CreateEntityFromCredential({
|
|
205
|
-
processAuthorizationCallback,
|
|
206
|
-
credentialRepository,
|
|
207
|
-
moduleDefinitions:
|
|
208
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
209
|
-
});
|
|
210
|
-
|
|
211
201
|
const router = express();
|
|
212
202
|
|
|
213
203
|
setIntegrationRoutes(router, authenticateUser, {
|
|
@@ -228,7 +218,6 @@ function createIntegrationRouter() {
|
|
|
228
218
|
getEntityOptionsById,
|
|
229
219
|
refreshEntityOptions,
|
|
230
220
|
processAuthorizationCallback,
|
|
231
|
-
createEntityFromCredential,
|
|
232
221
|
});
|
|
233
222
|
return router;
|
|
234
223
|
}
|
|
@@ -513,7 +502,6 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
513
502
|
getEntityOptionsById,
|
|
514
503
|
refreshEntityOptions,
|
|
515
504
|
processAuthorizationCallback,
|
|
516
|
-
createEntityFromCredential,
|
|
517
505
|
} = useCases;
|
|
518
506
|
|
|
519
507
|
router.route('/api/authorize').get(
|
|
@@ -578,19 +566,35 @@ function setEntityRoutes(router, authenticateUser, useCases) {
|
|
|
578
566
|
router.route('/api/entity').post(
|
|
579
567
|
catchAsyncError(async (req, res) => {
|
|
580
568
|
const user = await authenticateUser.execute(req);
|
|
569
|
+
const userId = user.getId();
|
|
581
570
|
const params = checkRequiredParams(req.body, [
|
|
582
571
|
'entityType',
|
|
583
572
|
'data',
|
|
584
573
|
]);
|
|
585
574
|
checkRequiredParams(req.body.data, ['credential_id']);
|
|
586
575
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
576
|
+
// May want to pass along the user ID as well so credential ID's can't be fished???
|
|
577
|
+
const credential = await getCredentialForUser.execute(
|
|
578
|
+
params.data.credential_id,
|
|
579
|
+
userId
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (!credential) {
|
|
583
|
+
throw Boom.badRequest('Invalid credential ID');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const module = await getModuleInstanceFromType.execute(
|
|
587
|
+
userId,
|
|
588
|
+
params.entityType
|
|
593
589
|
);
|
|
590
|
+
const entityDetails = await module.getEntityDetails(
|
|
591
|
+
module.api,
|
|
592
|
+
null,
|
|
593
|
+
null,
|
|
594
|
+
userId
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
res.json(await module.findOrCreateEntity(entityDetails));
|
|
594
598
|
})
|
|
595
599
|
);
|
|
596
600
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { MappingAlreadyExistsError } = require('../../errors');
|
|
2
3
|
const {
|
|
3
4
|
toObjectId,
|
|
4
5
|
fromObjectId,
|
|
@@ -180,6 +181,55 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
|
|
|
180
181
|
return this._mapMapping(decryptedMapping);
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
async createMapping(integrationId, sourceId, mapping) {
|
|
185
|
+
const now = new Date();
|
|
186
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
187
|
+
'IntegrationMapping',
|
|
188
|
+
{
|
|
189
|
+
integrationId,
|
|
190
|
+
sourceId:
|
|
191
|
+
sourceId === null || sourceId === undefined
|
|
192
|
+
? null
|
|
193
|
+
: String(sourceId),
|
|
194
|
+
mapping,
|
|
195
|
+
createdAt: now,
|
|
196
|
+
updatedAt: now,
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
let insertedId;
|
|
201
|
+
try {
|
|
202
|
+
insertedId = await insertOne(
|
|
203
|
+
this.prisma,
|
|
204
|
+
'IntegrationMapping',
|
|
205
|
+
encryptedDocument
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error.code === 11000) {
|
|
209
|
+
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const created = await findOne(this.prisma, 'IntegrationMapping', {
|
|
217
|
+
_id: insertedId,
|
|
218
|
+
});
|
|
219
|
+
if (!created) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Failed to create mapping: Document not found after insert. ' +
|
|
222
|
+
'This indicates a database consistency issue.'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return this._mapMapping(
|
|
226
|
+
await this.encryptionService.decryptFields(
|
|
227
|
+
'IntegrationMapping',
|
|
228
|
+
created
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
183
233
|
async findMappingsByIntegration(integrationId) {
|
|
184
234
|
const filter = {};
|
|
185
235
|
if (integrationId) filter.integrationId = integrationId;
|
|
@@ -39,6 +39,21 @@ class IntegrationMappingRepositoryInterface {
|
|
|
39
39
|
throw new Error('Method upsertMapping must be implemented by subclass');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Insert a mapping, failing if one already exists for the
|
|
44
|
+
* (integrationId, sourceId) pair. Atomic: usable as a claim/lock row.
|
|
45
|
+
*
|
|
46
|
+
* @param {string|number} integrationId - The integration ID
|
|
47
|
+
* @param {string} sourceId - The source ID for lookup
|
|
48
|
+
* @param {Object} mapping - The mapping data
|
|
49
|
+
* @returns {Promise<Object>} The created mapping document
|
|
50
|
+
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
51
|
+
* @abstract
|
|
52
|
+
*/
|
|
53
|
+
async createMapping(integrationId, sourceId, mapping) {
|
|
54
|
+
throw new Error('createMapping must be implemented by subclass');
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* Find all mappings for an integration
|
|
44
59
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { MappingAlreadyExistsError } = require('../../errors');
|
|
2
3
|
const {
|
|
3
4
|
assertMappingWrittenUnencrypted,
|
|
4
5
|
decryptQueriedMappings,
|
|
@@ -93,6 +94,34 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
|
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
99
|
+
*
|
|
100
|
+
* @param {string} integrationId - The integration ID
|
|
101
|
+
* @param {string} sourceId - The source ID
|
|
102
|
+
* @param {Object} mapping - The mapping data
|
|
103
|
+
* @returns {Promise<Object>} The created mapping document
|
|
104
|
+
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
105
|
+
*/
|
|
106
|
+
async createMapping(integrationId, sourceId, mapping) {
|
|
107
|
+
try {
|
|
108
|
+
return await this.prisma.integrationMapping.create({
|
|
109
|
+
data: {
|
|
110
|
+
integrationId,
|
|
111
|
+
sourceId: this._toString(sourceId),
|
|
112
|
+
mapping,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === 'P2002') {
|
|
117
|
+
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
118
|
+
cause: error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
/**
|
|
97
126
|
* Find all mappings for an integration
|
|
98
127
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { MappingAlreadyExistsError } = require('../../errors');
|
|
2
3
|
const {
|
|
3
4
|
assertMappingWrittenUnencrypted,
|
|
4
5
|
decryptQueriedMappings,
|
|
@@ -150,6 +151,35 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
150
151
|
return this._convertMappingIds(result);
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
156
|
+
*
|
|
157
|
+
* @param {string} integrationId - The integration ID
|
|
158
|
+
* @param {string} sourceId - The source ID
|
|
159
|
+
* @param {Object} mapping - The mapping data
|
|
160
|
+
* @returns {Promise<Object>} The created mapping document
|
|
161
|
+
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
162
|
+
*/
|
|
163
|
+
async createMapping(integrationId, sourceId, mapping) {
|
|
164
|
+
try {
|
|
165
|
+
const created = await this.prisma.integrationMapping.create({
|
|
166
|
+
data: {
|
|
167
|
+
integrationId: this._stringToInt(integrationId),
|
|
168
|
+
sourceId: this._toString(sourceId),
|
|
169
|
+
mapping,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return this._convertMappingIds(created);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code === 'P2002') {
|
|
175
|
+
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
176
|
+
cause: error,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
153
183
|
/**
|
|
154
184
|
* Find all mappings for an integration
|
|
155
185
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const { MappingAlreadyExistsError } = require('../../errors');
|
|
2
3
|
const {
|
|
3
4
|
IntegrationMappingRepositoryInterface,
|
|
4
5
|
} = require('./integration-mapping-repository-interface');
|
|
@@ -69,6 +70,34 @@ class IntegrationMappingRepository extends IntegrationMappingRepositoryInterface
|
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Insert a mapping; fails if (integrationId, sourceId) already exists
|
|
75
|
+
*
|
|
76
|
+
* @param {string} integrationId - The integration ID
|
|
77
|
+
* @param {string} sourceId - The source ID
|
|
78
|
+
* @param {Object} mapping - The mapping data
|
|
79
|
+
* @returns {Promise<Object>} The created mapping document
|
|
80
|
+
* @throws {MappingAlreadyExistsError} If the pair already exists
|
|
81
|
+
*/
|
|
82
|
+
async createMapping(integrationId, sourceId, mapping) {
|
|
83
|
+
try {
|
|
84
|
+
return await this.prisma.integrationMapping.create({
|
|
85
|
+
data: {
|
|
86
|
+
integrationId,
|
|
87
|
+
sourceId,
|
|
88
|
+
mapping,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code === 'P2002') {
|
|
93
|
+
throw new MappingAlreadyExistsError(integrationId, sourceId, {
|
|
94
|
+
cause: error,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
/**
|
|
73
102
|
* Find all mappings for an integration
|
|
74
103
|
* Replaces: IntegrationMapping.find({ integration: integrationId })
|
|
@@ -419,7 +419,7 @@ class IntegrationRepositoryMongo extends IntegrationRepositoryInterface {
|
|
|
419
419
|
for (const [key, value] of Object.entries(patch)) {
|
|
420
420
|
$set[`config.${key}`] = value;
|
|
421
421
|
}
|
|
422
|
-
$set.updatedAt = new Date();
|
|
422
|
+
$set.updatedAt = { $date: new Date().toISOString() };
|
|
423
423
|
|
|
424
424
|
const result = await this.prisma.$runCommandRaw({
|
|
425
425
|
findAndModify: 'Integration',
|
|
@@ -120,7 +120,7 @@ class ProcessRepositoryMongo extends ProcessRepositoryInterface {
|
|
|
120
120
|
if (normalized.newState !== null) {
|
|
121
121
|
$set.state = normalized.newState;
|
|
122
122
|
}
|
|
123
|
-
$set.updatedAt = new Date();
|
|
123
|
+
$set.updatedAt = { $date: new Date().toISOString() };
|
|
124
124
|
update.$set = $set;
|
|
125
125
|
|
|
126
126
|
if (Object.keys(normalized.pushSlice).length > 0) {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const {
|
|
3
|
-
findModuleDefinitionForEntity,
|
|
4
|
-
} = require('../utils/find-module-definition');
|
|
5
2
|
|
|
6
3
|
class GetEntityOptionsById {
|
|
7
4
|
/**
|
|
@@ -48,10 +45,11 @@ class GetEntityOptionsById {
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
const entityType = entity.moduleName;
|
|
51
|
-
const moduleDefinition =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const moduleDefinition = this.moduleDefinitions.find((def) => {
|
|
49
|
+
const modelName =
|
|
50
|
+
Module.getEntityModelFromDefinition(def).modelName;
|
|
51
|
+
return entityType === modelName;
|
|
52
|
+
});
|
|
55
53
|
|
|
56
54
|
if (!moduleDefinition) {
|
|
57
55
|
throw new Error(
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const {
|
|
3
|
-
findModuleDefinitionForEntity,
|
|
4
|
-
} = require('../utils/find-module-definition');
|
|
5
2
|
|
|
6
3
|
class GetModule {
|
|
7
4
|
constructor({ moduleRepository, moduleDefinitions }) {
|
|
@@ -45,10 +42,10 @@ class GetModule {
|
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
const entityType = entity.moduleName;
|
|
48
|
-
const moduleDefinition =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
45
|
+
const moduleDefinition = this.moduleDefinitions.find((def) => {
|
|
46
|
+
const modelName = Module.getEntityModelFromDefinition(def).modelName;
|
|
47
|
+
return entityType === modelName;
|
|
48
|
+
});
|
|
52
49
|
|
|
53
50
|
if (!moduleDefinition) {
|
|
54
51
|
throw new Error(
|
|
@@ -62,33 +59,15 @@ class GetModule {
|
|
|
62
59
|
definition: moduleDefinition,
|
|
63
60
|
});
|
|
64
61
|
|
|
65
|
-
//
|
|
66
|
-
// the response names it and says whether it still works, as the v2
|
|
67
|
-
// entity DTO does.
|
|
68
|
-
const credential = module.credential;
|
|
69
|
-
const credentialId =
|
|
70
|
-
credential && typeof credential === 'object'
|
|
71
|
-
? credential.id
|
|
72
|
-
: credential;
|
|
73
|
-
const ownerId = module.entity.userId ?? module.entity.user;
|
|
62
|
+
// todo: this properties should be methods in the Module class
|
|
74
63
|
return {
|
|
75
64
|
id: module.entity.id,
|
|
76
65
|
name: module.entity.name,
|
|
77
66
|
moduleName: module.entity.moduleName,
|
|
78
|
-
|
|
79
|
-
credentialId === undefined || credentialId === null
|
|
80
|
-
? null
|
|
81
|
-
: String(credentialId),
|
|
82
|
-
authIsValid:
|
|
83
|
-
typeof credential?.authIsValid === 'boolean'
|
|
84
|
-
? credential.authIsValid
|
|
85
|
-
: null,
|
|
67
|
+
credential: module.credential,
|
|
86
68
|
externalId: module.entity.externalId,
|
|
87
|
-
userId:
|
|
88
|
-
|
|
89
|
-
? null
|
|
90
|
-
: String(ownerId),
|
|
91
|
-
};
|
|
69
|
+
userId: module.entity.user.toString(),
|
|
70
|
+
}
|
|
92
71
|
}
|
|
93
72
|
}
|
|
94
73
|
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const {
|
|
3
|
-
findModuleDefinitionForEntity,
|
|
4
|
-
} = require('../utils/find-module-definition');
|
|
5
2
|
|
|
6
3
|
class RefreshEntityOptions {
|
|
7
4
|
/**
|
|
@@ -49,10 +46,11 @@ class RefreshEntityOptions {
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
const entityType = entity.moduleName;
|
|
52
|
-
const moduleDefinition =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
const moduleDefinition = this.moduleDefinitions.find((def) => {
|
|
50
|
+
const modelName =
|
|
51
|
+
Module.getEntityModelFromDefinition(def).modelName;
|
|
52
|
+
return entityType === modelName;
|
|
53
|
+
});
|
|
56
54
|
|
|
57
55
|
if (!moduleDefinition) {
|
|
58
56
|
throw new Error(
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
-
const {
|
|
3
|
-
findModuleDefinitionForEntity,
|
|
4
|
-
} = require('../utils/find-module-definition');
|
|
5
2
|
|
|
6
3
|
class TestModuleAuth {
|
|
7
4
|
/**
|
|
@@ -48,10 +45,11 @@ class TestModuleAuth {
|
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
const entityType = entity.moduleName;
|
|
51
|
-
const moduleDefinition =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const moduleDefinition = this.moduleDefinitions.find((def) => {
|
|
49
|
+
const modelName =
|
|
50
|
+
Module.getEntityModelFromDefinition(def).modelName;
|
|
51
|
+
return entityType === modelName;
|
|
52
|
+
});
|
|
55
53
|
|
|
56
54
|
if (!moduleDefinition) {
|
|
57
55
|
throw new Error(
|
package/package.json
CHANGED
|
@@ -1,15 +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",
|
|
11
|
+
"@aws-sdk/client-scheduler": "^3.1153.0",
|
|
10
12
|
"@aws-sdk/client-sqs": "^3.588.0",
|
|
11
13
|
"@aws-sdk/client-ssm": "^3.588.0",
|
|
12
14
|
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
|
15
|
+
"@friggframework/schemas": "2.0.0--canary.702.ef0dc8c.0",
|
|
13
16
|
"@hapi/boom": "^10.0.1",
|
|
14
17
|
"@opentelemetry/api": "^1.9.1",
|
|
15
18
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -47,9 +50,9 @@
|
|
|
47
50
|
}
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
52
|
-
"@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",
|
|
53
56
|
"@prisma/client": "^6.19.3",
|
|
54
57
|
"@types/lodash": "4.17.15",
|
|
55
58
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -85,9 +88,8 @@
|
|
|
85
88
|
"url": "https://github.com/friggframework/frigg/issues"
|
|
86
89
|
},
|
|
87
90
|
"homepage": "https://github.com/friggframework/frigg#readme",
|
|
88
|
-
"description": "",
|
|
89
91
|
"publishConfig": {
|
|
90
92
|
"access": "public"
|
|
91
93
|
},
|
|
92
|
-
"gitHead": "
|
|
94
|
+
"gitHead": "ef0dc8cb6c6f56048a61b6759faad7322046925d"
|
|
93
95
|
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
const { Module } = require('../module');
|
|
3
|
-
|
|
4
|
-
const sameId = (a, b) =>
|
|
5
|
-
a !== undefined &&
|
|
6
|
-
a !== null &&
|
|
7
|
-
b !== undefined &&
|
|
8
|
-
b !== null &&
|
|
9
|
-
String(a) === String(b);
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates (or finds) the entity for a credential the caller already holds:
|
|
13
|
-
* the v1 `POST /api/entity`.
|
|
14
|
-
*
|
|
15
|
-
* The module is built with the stored credential loaded, so the API can call
|
|
16
|
-
* the provider and a token refresh during `getEntityDetails` updates that
|
|
17
|
-
* credential. Persistence reuses `ProcessAuthorizationCallback.findOrCreateEntity`,
|
|
18
|
-
* the same find-or-create (and credential repointing) the authorization flow
|
|
19
|
-
* uses, so the two paths cannot drift.
|
|
20
|
-
*/
|
|
21
|
-
class CreateEntityFromCredential {
|
|
22
|
-
/**
|
|
23
|
-
* @param {Object} params
|
|
24
|
-
* @param {import('./process-authorization-callback').ProcessAuthorizationCallback} params.processAuthorizationCallback
|
|
25
|
-
* @param {Object} params.credentialRepository
|
|
26
|
-
* @param {Array<Object>} params.moduleDefinitions
|
|
27
|
-
* @param {Function} [params.createModule] - builds a Module; injectable for tests
|
|
28
|
-
*/
|
|
29
|
-
constructor({
|
|
30
|
-
processAuthorizationCallback,
|
|
31
|
-
credentialRepository,
|
|
32
|
-
moduleDefinitions,
|
|
33
|
-
createModule,
|
|
34
|
-
}) {
|
|
35
|
-
this.processAuthorizationCallback = processAuthorizationCallback;
|
|
36
|
-
this.credentialRepository = credentialRepository;
|
|
37
|
-
this.moduleDefinitions = moduleDefinitions || [];
|
|
38
|
-
this.createModule = createModule || ((params) => new Module(params));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {Object} params
|
|
43
|
-
* @param {Object} params.user - authenticated User
|
|
44
|
-
* @param {string} params.entityType
|
|
45
|
-
* @param {string} params.credentialId
|
|
46
|
-
* @returns {Promise<Object>} the persisted entity
|
|
47
|
-
*/
|
|
48
|
-
async execute({ user, entityType, credentialId }) {
|
|
49
|
-
const userId = user?.getId?.();
|
|
50
|
-
if (userId === undefined || userId === null) {
|
|
51
|
-
throw Boom.unauthorized('No authenticated user');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const definition = this.moduleDefinitions.find(
|
|
55
|
-
(def) => def && def.moduleName === entityType
|
|
56
|
-
);
|
|
57
|
-
if (!definition) {
|
|
58
|
-
throw Boom.badRequest(`Unknown entity type ${entityType}`, {
|
|
59
|
-
code: 'ENTITY_TYPE_NOT_FOUND',
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const credential =
|
|
64
|
-
await this.credentialRepository.findCredentialById(credentialId);
|
|
65
|
-
// A missing credential and someone else's get the same answer, so
|
|
66
|
-
// credential ids cannot be probed.
|
|
67
|
-
if (!credential || !sameId(credential.userId, userId)) {
|
|
68
|
-
throw Boom.badRequest('Invalid credential ID', {
|
|
69
|
-
code: 'INVALID_CREDENTIAL',
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// The repository returns the tokens flat; a Module reads them from
|
|
74
|
-
// `credential.data`.
|
|
75
|
-
const { id, userId: ownerId, externalId, authIsValid, ...data } =
|
|
76
|
-
credential;
|
|
77
|
-
const module = this.createModule({
|
|
78
|
-
userId,
|
|
79
|
-
definition,
|
|
80
|
-
entity: {
|
|
81
|
-
credential: { id, userId: ownerId, externalId, authIsValid, data },
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const entityDetails =
|
|
86
|
-
await definition.requiredAuthMethods.getEntityDetails(
|
|
87
|
-
module.api,
|
|
88
|
-
null,
|
|
89
|
-
null,
|
|
90
|
-
userId
|
|
91
|
-
);
|
|
92
|
-
Object.assign(
|
|
93
|
-
entityDetails.details,
|
|
94
|
-
module.apiParamsFromEntity(module.api)
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
return this.processAuthorizationCallback.findOrCreateEntity(
|
|
98
|
-
entityDetails,
|
|
99
|
-
definition.moduleName,
|
|
100
|
-
credential.id
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
module.exports = { CreateEntityFromCredential };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The API module definition a stored entity belongs to.
|
|
3
|
-
*
|
|
4
|
-
* Entities are persisted with `moduleName` set to the definition's
|
|
5
|
-
* `moduleName` (see ProcessAuthorizationCallback.findOrCreateEntity). A
|
|
6
|
-
* definition's legacy `modelName` is accepted as well, for entities written
|
|
7
|
-
* before the two were the same.
|
|
8
|
-
*
|
|
9
|
-
* @param {Array<Object>} moduleDefinitions
|
|
10
|
-
* @param {Object} entity - entity record with a `moduleName`
|
|
11
|
-
* @returns {Object|undefined}
|
|
12
|
-
*/
|
|
13
|
-
function findModuleDefinitionForEntity(moduleDefinitions, entity) {
|
|
14
|
-
const entityType = entity?.moduleName;
|
|
15
|
-
if (!entityType) return undefined;
|
|
16
|
-
return (moduleDefinitions || []).find(
|
|
17
|
-
(def) =>
|
|
18
|
-
def &&
|
|
19
|
-
(def.moduleName === entityType || def.modelName === entityType)
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
module.exports = { findModuleDefinitionForEntity };
|