@friggframework/core 2.0.0--canary.651.bc0e60a.0 → 2.0.0--canary.651.8ef5a7a.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 +33 -12
- package/database/documentdb-encryption-service.js +6 -2
- package/database/documentdb-utils.js +2 -1
- package/database/encryption/README.md +6 -5
- package/database/encryption/integration-mapping-encryption.js +18 -0
- package/integrations/repositories/integration-mapping-query-pipeline.js +152 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +53 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +6 -2
- package/integrations/repositories/integration-mapping-repository-mongo.js +69 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +2 -16
- package/package.json +5 -5
package/CLAUDE.md
CHANGED
|
@@ -224,18 +224,39 @@ packages/core/
|
|
|
224
224
|
'asc' | 'desc' }`, nulls last, ties broken by id in the same direction;
|
|
225
225
|
without it rows come in id order. `take` is 1–500. `omit` lists top-level
|
|
226
226
|
mapping keys to leave out of the rows; never write such rows back. Path
|
|
227
|
-
segments must match `^[A-Za-z_][A-Za-z0-9_]*$`.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
227
|
+
segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
|
|
228
|
+
DocumentDB adapters give the same pages and totals;
|
|
229
|
+
`integration-mapping-repository-query-parity.test.js` checks that against
|
|
230
|
+
real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
|
|
231
|
+
`QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
|
|
232
|
+
strings compare by the database collation on PostgreSQL and by code point
|
|
233
|
+
on MongoDB and DocumentDB, and arrays or objects at the sort path order
|
|
234
|
+
among themselves only on PostgreSQL (the other adapters fall back to id).
|
|
235
|
+
The legacy `IntegrationMappingRepository` inherits the port's
|
|
236
|
+
"not supported by this database adapter yet" error. Every adapter refuses
|
|
237
|
+
to run while field-level encryption still encrypts
|
|
238
|
+
`IntegrationMapping.mapping` on write (see `database/encryption/README.md`).
|
|
239
|
+
Validation lives in `integration-mapping-query.js`; a new operator is one
|
|
240
|
+
entry in its `OPERATORS` table, one in the Postgres adapter's
|
|
241
|
+
`CONDITION_SQL` and one in `CONDITION_EXPRESSIONS` in
|
|
242
|
+
`integration-mapping-query-pipeline.js`, the aggregation stages both
|
|
243
|
+
MongoDB-protocol adapters share. Those stages may only use what Amazon
|
|
244
|
+
DocumentDB 4.0 and 5.0 support (no `$facet`, `$getField`, `$set` or
|
|
245
|
+
`$unset`).
|
|
246
|
+
**Cost**: PostgreSQL answers in one SQL statement, which reads every row
|
|
247
|
+
of the integration and evaluates the JSON paths per row, because no JSON
|
|
248
|
+
index exists. With ~4 KB mappings on PostgreSQL 16 that is about 0.3 s per
|
|
249
|
+
call at 10⁴ rows per integration and 2.5–3 s at 10⁵. MongoDB answers in one
|
|
250
|
+
aggregate: the `integrationId` index narrows `$match` to the integration,
|
|
251
|
+
the `$expr` is then evaluated per document, and `$facet` returns the page
|
|
252
|
+
with its `$count`, so a page (after `omit`) must fit the 16 MB document
|
|
253
|
+
limit. DocumentDB has no `$facet`: the page and the count are two
|
|
254
|
+
concurrent aggregates, so the total does not come from the same snapshot
|
|
255
|
+
as the page. Both sort in memory on a computed key (`allowDiskUse`). With
|
|
256
|
+
~4 KB mappings on MongoDB 7 that is about 0.05–0.1 s per call at 10⁴ rows
|
|
257
|
+
per integration and 0.5–0.7 s at 10⁵ (the DocumentDB pipeline, run on
|
|
258
|
+
MongoDB, 0.6–0.8 s). On every adapter a deep offset or an empty page past
|
|
259
|
+
the end costs about the same as the first page.
|
|
239
260
|
- `process-repository-*.js` - Process (long-running job) persistence.
|
|
240
261
|
Implements `applyProcessUpdate(processId, ops)` — a race-safe alternative
|
|
241
262
|
to `update(id, patch)` that routes increments, sets, and bounded-array
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
getEncryptedFields,
|
|
4
|
+
getFieldsToEncryptOnWrite,
|
|
5
|
+
loadCustomEncryptionSchema,
|
|
6
|
+
} = require('./encryption/encryption-schema-registry');
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Encryption service specifically for DocumentDB repositories
|
|
@@ -107,7 +111,7 @@ class DocumentDBEncryptionService {
|
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
// Get encrypted fields from registry
|
|
110
|
-
const encryptedFieldsConfig =
|
|
114
|
+
const encryptedFieldsConfig = getFieldsToEncryptOnWrite(modelName);
|
|
111
115
|
if (!encryptedFieldsConfig || encryptedFieldsConfig.length === 0) {
|
|
112
116
|
return document;
|
|
113
117
|
}
|
|
@@ -166,11 +166,12 @@ async function findManyDrained(client, collection, filter = {}, options = {}) {
|
|
|
166
166
|
return drainCursor(client, collection, first);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
async function aggregateDrained(client, collection, pipeline) {
|
|
169
|
+
async function aggregateDrained(client, collection, pipeline, options = {}) {
|
|
170
170
|
const first = await client.$runCommandRaw({
|
|
171
171
|
aggregate: collection,
|
|
172
172
|
pipeline,
|
|
173
173
|
cursor: { batchSize: DRAIN_BATCH_SIZE },
|
|
174
|
+
...options,
|
|
174
175
|
});
|
|
175
176
|
return drainCursor(client, collection, first);
|
|
176
177
|
}
|
|
@@ -814,11 +814,12 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
|
|
|
814
814
|
❌ Query on encrypted fields (not supported)
|
|
815
815
|
❌ Manually decrypt data (use extension)
|
|
816
816
|
|
|
817
|
-
`
|
|
818
|
-
inside the `mapping` JSON, so it refuses to
|
|
819
|
-
still encrypts `IntegrationMapping.mapping` on
|
|
820
|
-
app definition, together with any nested
|
|
821
|
-
`encryption.schema` encrypts
|
|
817
|
+
The integration mapping repositories' `queryMappings()` (PostgreSQL, MongoDB
|
|
818
|
+
and DocumentDB) filters and sorts inside the `mapping` JSON, so it refuses to
|
|
819
|
+
run while field-level encryption still encrypts `IntegrationMapping.mapping` on
|
|
820
|
+
write. Opt the field out in the app definition, together with any nested
|
|
821
|
+
`mapping.*` path that `encryption.schema` encrypts; the Prisma extension and
|
|
822
|
+
`DocumentDBEncryptionService` both honor the opt-out:
|
|
822
823
|
|
|
823
824
|
```javascript
|
|
824
825
|
encryption: {
|
|
@@ -21,6 +21,23 @@ function getMappingFieldsEncryptedOnWrite() {
|
|
|
21
21
|
return fields;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @throws {Error} When field-level encryption still encrypts `mapping`, or a
|
|
26
|
+
* nested `mapping.*` path, on write, naming the opt-out that lifts it
|
|
27
|
+
*/
|
|
28
|
+
function assertMappingWrittenUnencrypted() {
|
|
29
|
+
const fields = getMappingFieldsEncryptedOnWrite();
|
|
30
|
+
if (fields.length === 0) return;
|
|
31
|
+
|
|
32
|
+
const encrypted = fields
|
|
33
|
+
.map((field) => `IntegrationMapping.${field}`)
|
|
34
|
+
.join(', ');
|
|
35
|
+
const optOut = fields.map((field) => `'${field}'`).join(', ');
|
|
36
|
+
throw new Error(
|
|
37
|
+
`queryMappings: field-level encryption still encrypts ${encrypted} on write, so it cannot be queried. Opt out by adding ${optOut} to appDefinition.encryption.disable.IntegrationMapping.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
function encryptedMappingFields() {
|
|
25
42
|
if (!getEncryptionConfig().enabled) return [];
|
|
26
43
|
|
|
@@ -36,6 +53,7 @@ function resetMappingEncryptionCheck() {
|
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
module.exports = {
|
|
56
|
+
assertMappingWrittenUnencrypted,
|
|
39
57
|
getMappingFieldsEncryptedOnWrite,
|
|
40
58
|
resetMappingEncryptionCheck,
|
|
41
59
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const SORT_KEY = '__sortKey';
|
|
2
|
+
const MONGO_DIRECTIONS = { asc: 1, desc: -1 };
|
|
3
|
+
const SORT_TYPE_RANKS = [
|
|
4
|
+
['string'],
|
|
5
|
+
['int', 'long', 'double', 'decimal'],
|
|
6
|
+
['bool'],
|
|
7
|
+
['array'],
|
|
8
|
+
['object'],
|
|
9
|
+
];
|
|
10
|
+
const SCALAR_TYPES = SORT_TYPE_RANKS.slice(0, 3).flat();
|
|
11
|
+
|
|
12
|
+
const isType = (expression, type) => ({ $eq: [{ $type: expression }, type] });
|
|
13
|
+
const isAbsent = (expression) => ({
|
|
14
|
+
$eq: [{ $ifNull: [expression, null] }, null],
|
|
15
|
+
});
|
|
16
|
+
const mappingField = (segments) => `$mapping.${segments.join('.')}`;
|
|
17
|
+
|
|
18
|
+
const CONDITION_EXPRESSIONS = {
|
|
19
|
+
exists: (operand) => ({ $ne: [{ $ifNull: [operand, null] }, null] }),
|
|
20
|
+
notExists: (operand) => isAbsent(operand),
|
|
21
|
+
in: (operand, values) => ({
|
|
22
|
+
$and: [
|
|
23
|
+
isType(operand, 'string'),
|
|
24
|
+
{ $in: [operand, { $literal: values }] },
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
notStartsWith: (operand, prefix) => ({
|
|
28
|
+
$cond: [
|
|
29
|
+
isType(operand, 'string'),
|
|
30
|
+
{
|
|
31
|
+
$ne: [
|
|
32
|
+
{ $substrCP: [operand, 0, [...prefix].length] },
|
|
33
|
+
{ $literal: prefix },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
true,
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Aggregation stages that answer a validated queryMappings query on the
|
|
43
|
+
* MongoDB wire protocol.
|
|
44
|
+
*
|
|
45
|
+
* @param {*} integrationId - The value IntegrationMapping.integrationId is stored as
|
|
46
|
+
* @param {ReturnType<import('./integration-mapping-query').validateMappingQuery>} query
|
|
47
|
+
* @returns {{match: Object, page: Object[], sort: Object}} `match` selects
|
|
48
|
+
* the matching documents; `page` sorts, skips and limits them; `sort` is
|
|
49
|
+
* the $sort stage inside `page`
|
|
50
|
+
*/
|
|
51
|
+
function buildMappingQueryStages(
|
|
52
|
+
integrationId,
|
|
53
|
+
{ where, orderBy, skip, take, omit }
|
|
54
|
+
) {
|
|
55
|
+
const match = {
|
|
56
|
+
$match: {
|
|
57
|
+
integrationId,
|
|
58
|
+
$expr: {
|
|
59
|
+
$and: [
|
|
60
|
+
isType('$mapping', 'object'),
|
|
61
|
+
...where.map(entryExpression),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const sort = { $sort: sortSpec(orderBy) };
|
|
67
|
+
const page = [
|
|
68
|
+
...(orderBy
|
|
69
|
+
? [{ $addFields: { [SORT_KEY]: sortKey(orderBy.path) } }]
|
|
70
|
+
: []),
|
|
71
|
+
...(omit.length > 0 ? [{ $project: omitProjection(omit) }] : []),
|
|
72
|
+
sort,
|
|
73
|
+
...(skip > 0 ? [{ $skip: skip }] : []),
|
|
74
|
+
{ $limit: take },
|
|
75
|
+
];
|
|
76
|
+
return { match, page, sort };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entryExpression(entry) {
|
|
80
|
+
if (!entry.anyOf) return conditionExpression(entry);
|
|
81
|
+
return { $or: entry.anyOf.map(conditionExpression) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function conditionExpression({ field, path, op, value }) {
|
|
85
|
+
const operand = field === 'sourceId' ? '$sourceId' : resolvePath(path);
|
|
86
|
+
return CONDITION_EXPRESSIONS[op](operand, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The value at a mapping path; null when an enclosing value is not an object.
|
|
91
|
+
*/
|
|
92
|
+
function resolvePath(segments) {
|
|
93
|
+
const enclosing = segments
|
|
94
|
+
.slice(0, -1)
|
|
95
|
+
.map((_, i) =>
|
|
96
|
+
isType(mappingField(segments.slice(0, i + 1)), 'object')
|
|
97
|
+
);
|
|
98
|
+
if (enclosing.length === 0) return mappingField(segments);
|
|
99
|
+
return {
|
|
100
|
+
$cond: [
|
|
101
|
+
enclosing.length === 1 ? enclosing[0] : { $and: enclosing },
|
|
102
|
+
mappingField(segments),
|
|
103
|
+
null,
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Orders values like jsonb: strings < numbers < booleans < arrays < objects,
|
|
110
|
+
* scalars by value within their type. Null and absent values sort last in
|
|
111
|
+
* both directions.
|
|
112
|
+
*/
|
|
113
|
+
function sortKey(path) {
|
|
114
|
+
const type = { $type: '$$value' };
|
|
115
|
+
return {
|
|
116
|
+
$let: {
|
|
117
|
+
vars: { value: resolvePath(path) },
|
|
118
|
+
in: {
|
|
119
|
+
missing: isAbsent('$$value'),
|
|
120
|
+
rank: {
|
|
121
|
+
$switch: {
|
|
122
|
+
branches: SORT_TYPE_RANKS.map((types, i) => ({
|
|
123
|
+
case: { $in: [type, types] },
|
|
124
|
+
then: i + 1,
|
|
125
|
+
})),
|
|
126
|
+
default: 0,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
value: {
|
|
130
|
+
$cond: [{ $in: [type, SCALAR_TYPES] }, '$$value', null],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sortSpec(orderBy) {
|
|
138
|
+
if (!orderBy) return { _id: 1 };
|
|
139
|
+
const direction = MONGO_DIRECTIONS[orderBy.direction];
|
|
140
|
+
return {
|
|
141
|
+
[`${SORT_KEY}.missing`]: 1,
|
|
142
|
+
[`${SORT_KEY}.rank`]: direction,
|
|
143
|
+
[`${SORT_KEY}.value`]: direction,
|
|
144
|
+
_id: direction,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function omitProjection(omit) {
|
|
149
|
+
return Object.fromEntries(omit.map((key) => [`mapping.${key}`, 0]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { buildMappingQueryStages };
|
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
updateOne,
|
|
9
9
|
deleteOne,
|
|
10
10
|
deleteMany,
|
|
11
|
+
aggregate,
|
|
11
12
|
aggregateDrained,
|
|
12
13
|
} = require('../../database/documentdb-utils');
|
|
13
14
|
const {
|
|
@@ -16,6 +17,20 @@ const {
|
|
|
16
17
|
const {
|
|
17
18
|
DocumentDBEncryptionService,
|
|
18
19
|
} = require('../../database/documentdb-encryption-service');
|
|
20
|
+
const {
|
|
21
|
+
assertMappingWrittenUnencrypted,
|
|
22
|
+
} = require('../../database/encryption/integration-mapping-encryption');
|
|
23
|
+
const { validateMappingQuery } = require('./integration-mapping-query');
|
|
24
|
+
const {
|
|
25
|
+
buildMappingQueryStages,
|
|
26
|
+
} = require('./integration-mapping-query-pipeline');
|
|
27
|
+
|
|
28
|
+
function storedIntegrationId(id) {
|
|
29
|
+
if (!['string', 'number'].includes(typeof id) || id === '') {
|
|
30
|
+
throw new TypeError(`Invalid ID: ${id}`);
|
|
31
|
+
}
|
|
32
|
+
return String(id);
|
|
33
|
+
}
|
|
19
34
|
|
|
20
35
|
class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositoryInterface {
|
|
21
36
|
constructor() {
|
|
@@ -179,6 +194,44 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
|
|
|
179
194
|
return decryptedDocs.map((doc) => this._mapMapping(doc));
|
|
180
195
|
}
|
|
181
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} integrationId
|
|
199
|
+
* @param {Object} query - See IntegrationMappingRepositoryInterface.queryMappings
|
|
200
|
+
* @returns {Promise<{mappings: Array<Object>, total: number}>}
|
|
201
|
+
*/
|
|
202
|
+
async queryMappings(integrationId, query) {
|
|
203
|
+
const validated = validateMappingQuery(query);
|
|
204
|
+
const stored = storedIntegrationId(integrationId);
|
|
205
|
+
assertMappingWrittenUnencrypted();
|
|
206
|
+
const { match, page, sort } = buildMappingQueryStages(
|
|
207
|
+
stored,
|
|
208
|
+
validated
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const [docs, counts] = await Promise.all([
|
|
212
|
+
aggregateDrained(
|
|
213
|
+
this.prisma,
|
|
214
|
+
'IntegrationMapping',
|
|
215
|
+
[match, ...page, sort],
|
|
216
|
+
{ allowDiskUse: true }
|
|
217
|
+
),
|
|
218
|
+
aggregate(this.prisma, 'IntegrationMapping', [
|
|
219
|
+
match,
|
|
220
|
+
{ $count: 'total' },
|
|
221
|
+
]),
|
|
222
|
+
]);
|
|
223
|
+
const decryptedDocs = await Promise.all(
|
|
224
|
+
docs.map((doc) =>
|
|
225
|
+
this.encryptionService.decryptFields('IntegrationMapping', doc)
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
mappings: decryptedDocs.map((doc) => this._mapMapping(doc)),
|
|
231
|
+
total: counts[0]?.total ?? 0,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
182
235
|
async deleteMapping(integrationId, sourceId) {
|
|
183
236
|
const filter = this._compositeFilter(integrationId, sourceId);
|
|
184
237
|
const result = await deleteOne(
|
|
@@ -79,13 +79,17 @@ class IntegrationMappingRepositoryInterface {
|
|
|
79
79
|
* conditions, anyOf members included.
|
|
80
80
|
* @param {{path: string, direction: 'asc'|'desc'}} [query.orderBy] - A
|
|
81
81
|
* mapping path; nulls last, ties broken by id in the same direction.
|
|
82
|
-
*
|
|
82
|
+
* Values order string < number < boolean < array < object. Strings
|
|
83
|
+
* compare by the database collation on PostgreSQL and by code point on
|
|
84
|
+
* MongoDB and DocumentDB; arrays and objects order among themselves
|
|
85
|
+
* only on PostgreSQL. Without it rows are ordered by id ascending.
|
|
83
86
|
* @param {number} [query.skip=0] - Rows to skip (integer ≥ 0)
|
|
84
87
|
* @param {number} query.take - Page size (integer 1–500)
|
|
85
88
|
* @param {string[]} [query.omit=[]] - Top-level mapping keys to leave out of
|
|
86
89
|
* the returned rows; such projected rows must not be written back
|
|
87
90
|
* @returns {Promise<{mappings: Array<Object>, total: number}>} The page, and
|
|
88
|
-
* the number of rows matching `where`
|
|
91
|
+
* the number of rows matching `where` (counted by a separate command on
|
|
92
|
+
* DocumentDB, so not from the page's snapshot)
|
|
89
93
|
*/
|
|
90
94
|
async queryMappings(integrationId, query) {
|
|
91
95
|
throw new Error(
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
assertMappingWrittenUnencrypted,
|
|
4
|
+
} = require('../../database/encryption/integration-mapping-encryption');
|
|
2
5
|
const {
|
|
3
6
|
IntegrationMappingRepositoryInterface,
|
|
4
7
|
} = require('./integration-mapping-repository-interface');
|
|
8
|
+
const { validateMappingQuery } = require('./integration-mapping-query');
|
|
9
|
+
const {
|
|
10
|
+
buildMappingQueryStages,
|
|
11
|
+
} = require('./integration-mapping-query-pipeline');
|
|
12
|
+
|
|
13
|
+
const OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/;
|
|
14
|
+
|
|
15
|
+
const fromRawDate = (raw) => new Date(raw.$date);
|
|
16
|
+
|
|
17
|
+
function strictObjectId(id) {
|
|
18
|
+
if (typeof id !== 'string' || !OBJECT_ID_REGEX.test(id)) {
|
|
19
|
+
throw new TypeError(`Invalid ID: ${id} is not an ObjectId`);
|
|
20
|
+
}
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
/**
|
|
7
25
|
* MongoDB Integration Mapping Repository Adapter
|
|
@@ -155,6 +173,57 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
|
|
|
155
173
|
return counts;
|
|
156
174
|
}
|
|
157
175
|
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} integrationId
|
|
178
|
+
* @param {Object} query - See IntegrationMappingRepositoryInterface.queryMappings
|
|
179
|
+
* @returns {Promise<{mappings: Array<Object>, total: number}>}
|
|
180
|
+
*/
|
|
181
|
+
async queryMappings(integrationId, query) {
|
|
182
|
+
const validated = validateMappingQuery(query);
|
|
183
|
+
const objectId = strictObjectId(integrationId);
|
|
184
|
+
assertMappingWrittenUnencrypted();
|
|
185
|
+
const { match, page } = buildMappingQueryStages(
|
|
186
|
+
{ $oid: objectId },
|
|
187
|
+
validated
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const result = await this.prisma.$runCommandRaw({
|
|
191
|
+
aggregate: 'IntegrationMapping',
|
|
192
|
+
pipeline: [
|
|
193
|
+
match,
|
|
194
|
+
{
|
|
195
|
+
$facet: {
|
|
196
|
+
mappings: page,
|
|
197
|
+
total: [{ $count: 'total' }],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
cursor: {},
|
|
202
|
+
allowDiskUse: true,
|
|
203
|
+
});
|
|
204
|
+
const [{ mappings, total }] = result.cursor.firstBatch;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
mappings: mappings.map((doc) => this._fromRawMapping(doc)),
|
|
208
|
+
total: total[0]?.total ?? 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A raw aggregate document, in the shape findMappingsByIntegration returns.
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
_fromRawMapping(doc) {
|
|
217
|
+
return {
|
|
218
|
+
id: doc._id.$oid,
|
|
219
|
+
integrationId: doc.integrationId.$oid,
|
|
220
|
+
sourceId: doc.sourceId ?? null,
|
|
221
|
+
mapping: doc.mapping ?? null,
|
|
222
|
+
createdAt: fromRawDate(doc.createdAt),
|
|
223
|
+
updatedAt: fromRawDate(doc.updatedAt),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
158
227
|
/**
|
|
159
228
|
* Find mapping by ID
|
|
160
229
|
* @param {string} id - Mapping ID
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const {
|
|
3
|
-
|
|
3
|
+
assertMappingWrittenUnencrypted,
|
|
4
4
|
} = require('../../database/encryption/integration-mapping-encryption');
|
|
5
5
|
const {
|
|
6
6
|
IntegrationMappingRepositoryInterface,
|
|
@@ -247,7 +247,7 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
247
247
|
const { where, orderBy, skip, take, omit } =
|
|
248
248
|
validateMappingQuery(query);
|
|
249
249
|
const intIntegrationId = strictIntId(integrationId);
|
|
250
|
-
|
|
250
|
+
assertMappingWrittenUnencrypted();
|
|
251
251
|
|
|
252
252
|
const params = [];
|
|
253
253
|
const bind = (v) => {
|
|
@@ -294,20 +294,6 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
294
294
|
};
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
/** @private */
|
|
298
|
-
_assertMappingWrittenUnencrypted() {
|
|
299
|
-
const fields = getMappingFieldsEncryptedOnWrite();
|
|
300
|
-
if (fields.length === 0) return;
|
|
301
|
-
|
|
302
|
-
const encrypted = fields
|
|
303
|
-
.map((field) => `IntegrationMapping.${field}`)
|
|
304
|
-
.join(', ');
|
|
305
|
-
const optOut = fields.map((field) => `'${field}'`).join(', ');
|
|
306
|
-
throw new Error(
|
|
307
|
-
`queryMappings: field-level encryption still encrypts ${encrypted} on write, so it cannot be queried. Opt out by adding ${optOut} to appDefinition.encryption.disable.IntegrationMapping.`
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
297
|
/**
|
|
312
298
|
* One where entry: a condition, or an anyOf group as a parenthesized OR.
|
|
313
299
|
* @private
|
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--canary.651.
|
|
4
|
+
"version": "2.0.0--canary.651.8ef5a7a.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@friggframework/eslint-config": "2.0.0--canary.651.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0--canary.651.
|
|
53
|
-
"@friggframework/test": "2.0.0--canary.651.
|
|
51
|
+
"@friggframework/eslint-config": "2.0.0--canary.651.8ef5a7a.0",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0--canary.651.8ef5a7a.0",
|
|
53
|
+
"@friggframework/test": "2.0.0--canary.651.8ef5a7a.0",
|
|
54
54
|
"@prisma/client": "^6.19.3",
|
|
55
55
|
"@types/lodash": "4.17.15",
|
|
56
56
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"publishConfig": {
|
|
91
91
|
"access": "public"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "8ef5a7a4c048da70b48afa1469fe63a71d9f1240"
|
|
94
94
|
}
|