@friggframework/core 2.0.0-next.115 → 2.0.0-next.117
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/index.js +2 -0
- package/integrations/integration-base.js +29 -0
- 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/package.json +5 -5
|
@@ -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 }) {
|
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,
|
|
@@ -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
|
*/
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.117",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
51
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
52
|
-
"@friggframework/test": "2.0.0-next.
|
|
50
|
+
"@friggframework/eslint-config": "2.0.0-next.117",
|
|
51
|
+
"@friggframework/prettier-config": "2.0.0-next.117",
|
|
52
|
+
"@friggframework/test": "2.0.0-next.117",
|
|
53
53
|
"@prisma/client": "^6.19.3",
|
|
54
54
|
"@types/lodash": "4.17.15",
|
|
55
55
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "27771927dd746cc517d0c09189ce63b6e859e8bc"
|
|
93
93
|
}
|