@friggframework/core 2.0.0--canary.651.87132bf.0 → 2.0.0--canary.651.bc0e60a.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +25 -1
- package/database/encryption/README.md +16 -0
- package/database/encryption/integration-mapping-encryption.js +41 -0
- package/integrations/repositories/integration-mapping-query.js +61 -44
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -8
- package/integrations/repositories/integration-mapping-repository-interface.js +3 -7
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -8
- package/integrations/repositories/integration-mapping-repository-postgres.js +66 -94
- package/integrations/repositories/integration-mapping-repository.js +0 -11
- package/package.json +5 -5
package/CLAUDE.md
CHANGED
|
@@ -211,7 +211,31 @@ packages/core/
|
|
|
211
211
|
- `integration-repository-factory.js` - Creates database-specific repositories
|
|
212
212
|
- `integration-repository-mongo.js` - MongoDB implementation
|
|
213
213
|
- `integration-repository-postgres.js` - PostgreSQL implementation
|
|
214
|
-
- `integration-mapping-repository-*.js` - Mapping data persistence
|
|
214
|
+
- `integration-mapping-repository-*.js` - Mapping data persistence.
|
|
215
|
+
`queryMappings(integrationId, { where, orderBy, skip, take, omit })`
|
|
216
|
+
returns `{ mappings, total }`: one filtered, ordered page of an
|
|
217
|
+
integration's mappings, for callers that cannot load every row through
|
|
218
|
+
`findMappingsByIntegration`. `where` is an array of ANDed conditions
|
|
219
|
+
(at most 20); an entry may be `{ anyOf: [...] }`, ORed, one level deep.
|
|
220
|
+
Conditions are `{ path: 'mapping.<segment>...', op: 'exists' | 'notExists' }`
|
|
221
|
+
(JSON null counts as absent), `{ path: 'mapping.…', op: 'in', value: string[] }`
|
|
222
|
+
(1–500 strings) and `{ path: 'sourceId', op: 'notStartsWith', value }`
|
|
223
|
+
(a NULL sourceId matches). `orderBy` is `{ path: 'mapping.…', direction:
|
|
224
|
+
'asc' | 'desc' }`, nulls last, ties broken by id in the same direction;
|
|
225
|
+
without it rows come in id order. `take` is 1–500. `omit` lists top-level
|
|
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_]*$`. **PostgreSQL only**: the
|
|
228
|
+
MongoDB, DocumentDB and legacy repositories inherit the port's
|
|
229
|
+
"not supported by this database adapter yet" error. It also refuses to run
|
|
230
|
+
while field-level encryption still encrypts `IntegrationMapping.mapping` on
|
|
231
|
+
write (see `database/encryption/README.md`). Validation lives in
|
|
232
|
+
`integration-mapping-query.js`; a new operator is one entry in its
|
|
233
|
+
`OPERATORS` table and one in the Postgres adapter's `CONDITION_SQL`.
|
|
234
|
+
**Cost**: one SQL statement per call, which reads every row of the
|
|
235
|
+
integration and evaluates the JSON paths per row, because no JSON index
|
|
236
|
+
exists. With ~4 KB mappings on PostgreSQL 16 that is about 0.3 s per call
|
|
237
|
+
at 10⁴ rows per integration and 2.5–3 s at 10⁵; a deep offset or an empty
|
|
238
|
+
page past the end costs about the same as the first page.
|
|
215
239
|
- `process-repository-*.js` - Process (long-running job) persistence.
|
|
216
240
|
Implements `applyProcessUpdate(processId, ops)` — a race-safe alternative
|
|
217
241
|
to `update(id, patch)` that routes increments, sets, and bounded-array
|
|
@@ -814,6 +814,22 @@ 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
|
+
`IntegrationMappingRepository.queryMappings()` (PostgreSQL) filters and sorts
|
|
818
|
+
inside the `mapping` JSON, so it refuses to run while field-level encryption
|
|
819
|
+
still encrypts `IntegrationMapping.mapping` on write. Opt the field out in the
|
|
820
|
+
app definition, together with any nested `mapping.*` path that
|
|
821
|
+
`encryption.schema` encrypts:
|
|
822
|
+
|
|
823
|
+
```javascript
|
|
824
|
+
encryption: {
|
|
825
|
+
disable: { IntegrationMapping: ['mapping'] },
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
The opt-out applies to writes only. Rows written encrypted before it stay
|
|
830
|
+
readable, but `queryMappings()` does not match them until they are written
|
|
831
|
+
again.
|
|
832
|
+
|
|
817
833
|
## Future Enhancements
|
|
818
834
|
|
|
819
835
|
### Planned
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { getEncryptionConfig } = require('../prisma');
|
|
2
|
+
const {
|
|
3
|
+
getFieldsToEncryptOnWrite,
|
|
4
|
+
loadCustomEncryptionSchema,
|
|
5
|
+
} = require('./encryption-schema-registry');
|
|
6
|
+
|
|
7
|
+
let mappingWrittenPlain = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `IntegrationMapping` fields, `mapping` itself or a nested `mapping.*`
|
|
11
|
+
* path, that field-level encryption encrypts on write. An empty result is
|
|
12
|
+
* kept for the life of the process.
|
|
13
|
+
*
|
|
14
|
+
* @returns {string[]} Empty when every mapping path is written as plain JSON
|
|
15
|
+
*/
|
|
16
|
+
function getMappingFieldsEncryptedOnWrite() {
|
|
17
|
+
if (mappingWrittenPlain) return [];
|
|
18
|
+
|
|
19
|
+
const fields = encryptedMappingFields();
|
|
20
|
+
mappingWrittenPlain = fields.length === 0;
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function encryptedMappingFields() {
|
|
25
|
+
if (!getEncryptionConfig().enabled) return [];
|
|
26
|
+
|
|
27
|
+
loadCustomEncryptionSchema();
|
|
28
|
+
return getFieldsToEncryptOnWrite('IntegrationMapping').filter(
|
|
29
|
+
(field) => field === 'mapping' || field.startsWith('mapping.')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Test helper: forget a kept result. */
|
|
34
|
+
function resetMappingEncryptionCheck() {
|
|
35
|
+
mappingWrittenPlain = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
getMappingFieldsEncryptedOnWrite,
|
|
40
|
+
resetMappingEncryptionCheck,
|
|
41
|
+
};
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation for IntegrationMappingRepository.queryMappings().
|
|
3
|
-
*
|
|
4
|
-
* Backend-agnostic: enforces the query shape BEFORE an adapter builds a
|
|
5
|
-
* database command, so every adapter rejects the same input. Paths come out
|
|
6
|
-
* split into identifier-only segments that adapters pass as bound
|
|
7
|
-
* parameters; nothing from the query is spliced into SQL text.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
const SEGMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
2
|
const MAX_TAKE = 500;
|
|
12
3
|
const MAX_IN_VALUES = 500;
|
|
13
4
|
const MAX_CONDITIONS = 20;
|
|
14
|
-
const
|
|
5
|
+
const DIRECTIONS = ['asc', 'desc'];
|
|
15
6
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
const OPERATORS = {
|
|
8
|
+
exists: { fields: ['mapping'] },
|
|
9
|
+
notExists: { fields: ['mapping'] },
|
|
10
|
+
in: { fields: ['mapping'], value: toStringList },
|
|
11
|
+
notStartsWith: { fields: ['sourceId'], value: toPrefix },
|
|
19
12
|
};
|
|
20
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks a queryMappings query and normalizes it for an adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} query - See IntegrationMappingRepositoryInterface.queryMappings
|
|
18
|
+
* @returns {{where: Array<Object>, orderBy: ({path: string[], direction: 'asc'|'desc'}|null), skip: number, take: number, omit: string[]}}
|
|
19
|
+
* @throws {Error} When the query does not fit that shape
|
|
20
|
+
*/
|
|
21
21
|
function validateMappingQuery(query) {
|
|
22
22
|
if (!isPlainObject(query)) {
|
|
23
23
|
throw new Error('queryMappings: query must be an object');
|
|
@@ -73,12 +73,12 @@ function toOrderBy(orderBy) {
|
|
|
73
73
|
"queryMappings: orderBy.path must be a mapping path ('mapping.<segment>...')"
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
|
-
if (!
|
|
76
|
+
if (!DIRECTIONS.includes(orderBy.direction)) {
|
|
77
77
|
throw new Error(
|
|
78
78
|
"queryMappings: orderBy.direction must be 'asc' or 'desc'"
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
|
-
return { segments, direction:
|
|
81
|
+
return { path: segments, direction: orderBy.direction };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function toWhereEntry(entry) {
|
|
@@ -110,45 +110,62 @@ function isPlainObject(value) {
|
|
|
110
110
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* `{ path: 'mapping.c2h.lastStatus', op: 'in', value: ['failed'] }` →
|
|
115
|
+
* `{ field: 'mapping', path: ['c2h', 'lastStatus'], op: 'in', value: ['failed'] }`.
|
|
116
|
+
* A `sourceId` condition has no `path`; an op without a value has no `value`.
|
|
117
|
+
*/
|
|
113
118
|
function toCondition({ path, op, value }) {
|
|
114
119
|
const { field, segments } = parsePath(path);
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
120
|
+
const operator = Object.hasOwn(OPERATORS, op) ? OPERATORS[op] : null;
|
|
121
|
+
if (!operator?.fields.includes(field)) {
|
|
117
122
|
throw new Error(
|
|
118
123
|
`queryMappings: op ${JSON.stringify(
|
|
119
124
|
op
|
|
120
|
-
)} is not allowed on '${path}' (allowed: ${
|
|
125
|
+
)} is not allowed on '${path}' (allowed: ${operatorsOn(field).join(
|
|
126
|
+
', '
|
|
127
|
+
)})`
|
|
121
128
|
);
|
|
122
129
|
}
|
|
130
|
+
return {
|
|
131
|
+
field,
|
|
132
|
+
...(segments.length > 0 && { path: segments }),
|
|
133
|
+
op,
|
|
134
|
+
...(operator.value && { value: operator.value(value, path) }),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`queryMappings: 'in' value must be a non-empty array of strings on '${path}'`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (value.length > MAX_IN_VALUES) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`queryMappings: 'in' value must have at most ${MAX_IN_VALUES} strings on '${path}'`
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
return { field, segments, op, value };
|
|
140
|
-
}
|
|
138
|
+
function operatorsOn(field) {
|
|
139
|
+
return Object.keys(OPERATORS).filter((op) =>
|
|
140
|
+
OPERATORS[op].fields.includes(field)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
function toStringList(value, path) {
|
|
145
|
+
if (
|
|
146
|
+
!Array.isArray(value) ||
|
|
147
|
+
value.length === 0 ||
|
|
148
|
+
!value.every((v) => typeof v === 'string')
|
|
149
|
+
) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`queryMappings: 'in' value must be a non-empty array of strings on '${path}'`
|
|
152
|
+
);
|
|
149
153
|
}
|
|
154
|
+
if (value.length > MAX_IN_VALUES) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`queryMappings: 'in' value must have at most ${MAX_IN_VALUES} strings on '${path}'`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
162
|
+
function toPrefix(value, path) {
|
|
163
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`queryMappings: 'notStartsWith' value must be a non-empty string on '${path}'`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
/**
|
|
@@ -173,4 +190,4 @@ function parsePath(path) {
|
|
|
173
190
|
return { field, segments };
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
module.exports = {
|
|
193
|
+
module.exports = { validateMappingQuery };
|
|
@@ -179,14 +179,6 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
|
|
|
179
179
|
return decryptedDocs.map((doc) => this._mapMapping(doc));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/**
|
|
183
|
-
* Not implemented for DocumentDB yet; only the PostgreSQL adapter supports it.
|
|
184
|
-
* @throws {Error} Always
|
|
185
|
-
*/
|
|
186
|
-
async queryMappings() {
|
|
187
|
-
throw new Error('queryMappings is not supported on DocumentDB yet');
|
|
188
|
-
}
|
|
189
|
-
|
|
190
182
|
async deleteMapping(integrationId, sourceId) {
|
|
191
183
|
const filter = this._compositeFilter(integrationId, sourceId);
|
|
192
184
|
const result = await deleteOne(
|
|
@@ -56,11 +56,6 @@ class IntegrationMappingRepositoryInterface {
|
|
|
56
56
|
* Query one filtered, ordered page of an integration's mappings without
|
|
57
57
|
* loading every row. Rows have the same shape as findMappingsByIntegration.
|
|
58
58
|
*
|
|
59
|
-
* Cost: each call is O(rows of the integration × mapping size) in the
|
|
60
|
-
* database, because JSON paths are evaluated per row and no JSON index
|
|
61
|
-
* exists. That is fine at ~10⁴ rows per integration and takes seconds per
|
|
62
|
-
* call at 10⁵+.
|
|
63
|
-
*
|
|
64
59
|
* Paths address the `mapping` JSON by identifier-only segments
|
|
65
60
|
* (`'mapping.c2h.lastStatus'`), or the `sourceId` column. Conditions:
|
|
66
61
|
* - `{ path: 'mapping.…', op: 'exists' | 'notExists' }` — JSON null counts
|
|
@@ -91,10 +86,11 @@ class IntegrationMappingRepositoryInterface {
|
|
|
91
86
|
* the returned rows; such projected rows must not be written back
|
|
92
87
|
* @returns {Promise<{mappings: Array<Object>, total: number}>} The page, and
|
|
93
88
|
* the number of rows matching `where`
|
|
94
|
-
* @abstract
|
|
95
89
|
*/
|
|
96
90
|
async queryMappings(integrationId, query) {
|
|
97
|
-
throw new Error(
|
|
91
|
+
throw new Error(
|
|
92
|
+
'queryMappings is not supported by this database adapter yet'
|
|
93
|
+
);
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
/**
|
|
@@ -87,14 +87,6 @@ class IntegrationMappingRepositoryMongo extends IntegrationMappingRepositoryInte
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
* Not implemented for MongoDB yet; only the PostgreSQL adapter supports it.
|
|
92
|
-
* @throws {Error} Always
|
|
93
|
-
*/
|
|
94
|
-
async queryMappings() {
|
|
95
|
-
throw new Error('queryMappings is not supported on MongoDB yet');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
90
|
/**
|
|
99
91
|
* Delete a mapping by integration and source ID
|
|
100
92
|
* Replaces: IntegrationMapping.deleteOne({ integration, sourceId })
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
const { prisma
|
|
1
|
+
const { prisma } = require('../../database/prisma');
|
|
2
2
|
const {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} = require('../../database/encryption/encryption-schema-registry');
|
|
3
|
+
getMappingFieldsEncryptedOnWrite,
|
|
4
|
+
} = require('../../database/encryption/integration-mapping-encryption');
|
|
6
5
|
const {
|
|
7
6
|
IntegrationMappingRepositoryInterface,
|
|
8
7
|
} = require('./integration-mapping-repository-interface');
|
|
9
8
|
const { strictIntId } = require('./report-id');
|
|
10
9
|
const { validateMappingQuery } = require('./integration-mapping-query');
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const COLUMNS = { mapping: '"mapping"', sourceId: '"sourceId"' };
|
|
12
|
+
const SQL_DIRECTIONS = { asc: 'ASC', desc: 'DESC' };
|
|
13
|
+
|
|
14
|
+
const jsonPathOperand = (column, path) => ({
|
|
15
|
+
json: `${column} #> ${path}::text[]`,
|
|
16
|
+
text: `${column} #>> ${path}::text[]`,
|
|
17
|
+
});
|
|
18
|
+
const jsonType = (json) => `COALESCE(jsonb_typeof(${json}), 'null')`;
|
|
19
|
+
|
|
20
|
+
const CONDITION_SQL = {
|
|
21
|
+
exists: ({ json }) => `${jsonType(json)} <> 'null'`,
|
|
22
|
+
notExists: ({ json }) => `${jsonType(json)} = 'null'`,
|
|
23
|
+
in: ({ json, text, value }) =>
|
|
24
|
+
`(jsonb_typeof(${json}) = 'string' AND ${text} = ANY(${value}::text[]))`,
|
|
25
|
+
notStartsWith: ({ text, value }) =>
|
|
26
|
+
`(${text} IS NULL OR NOT starts_with(${text}, ${value}::text))`,
|
|
27
|
+
};
|
|
16
28
|
|
|
17
29
|
/**
|
|
18
30
|
* PostgreSQL Integration Mapping Repository Adapter
|
|
@@ -227,15 +239,6 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
227
239
|
}
|
|
228
240
|
|
|
229
241
|
/**
|
|
230
|
-
* Query one page of an integration's mappings, filtering and ordering
|
|
231
|
-
* inside Postgres instead of loading every row. The total rides on the
|
|
232
|
-
* page as a window count, so one statement reads the matching rows once;
|
|
233
|
-
* only an empty page after the first needs a second, count-only statement.
|
|
234
|
-
*
|
|
235
|
-
* The SQL text is fixed: every caller value, JSON paths included (as
|
|
236
|
-
* text[]), is a positional parameter, and the sort direction comes from
|
|
237
|
-
* the ASC/DESC whitelist in integration-mapping-query.js.
|
|
238
|
-
*
|
|
239
242
|
* @param {string} integrationId
|
|
240
243
|
* @param {Object} query - See IntegrationMappingRepositoryInterface.queryMappings
|
|
241
244
|
* @returns {Promise<{mappings: Array<Object>, total: number}>}
|
|
@@ -257,79 +260,52 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
257
260
|
`jsonb_typeof("mapping") = 'object'`,
|
|
258
261
|
...where.map((entry) => this._whereEntrySql(entry, bind)),
|
|
259
262
|
].join(' AND ');
|
|
260
|
-
const
|
|
261
|
-
|
|
263
|
+
const orderSql = orderBy ? this._orderSql(orderBy, bind) : `"id" ASC`;
|
|
262
264
|
const mappingSql =
|
|
263
265
|
omit.length > 0
|
|
264
266
|
? `"mapping" - ${bind(omit)}::text[] AS "mapping"`
|
|
265
267
|
: `"mapping"`;
|
|
266
|
-
const orderSql = orderBy ? this._orderSql(orderBy, bind) : `"id" ASC`;
|
|
267
268
|
|
|
268
|
-
const
|
|
269
|
+
const sql = `
|
|
270
|
+
WITH "matched" AS (
|
|
271
|
+
SELECT "id", "integrationId", "sourceId", "mapping", "createdAt", "updatedAt"
|
|
272
|
+
FROM "IntegrationMapping"
|
|
273
|
+
WHERE ${whereSql}
|
|
274
|
+
),
|
|
275
|
+
"page" AS (
|
|
276
|
+
SELECT * FROM "matched"
|
|
277
|
+
ORDER BY ${orderSql}
|
|
278
|
+
OFFSET ${bind(skip)}::bigint
|
|
279
|
+
LIMIT ${bind(take)}::int
|
|
280
|
+
)
|
|
269
281
|
SELECT "id", "integrationId", "sourceId", ${mappingSql}, "createdAt", "updatedAt",
|
|
270
|
-
(COUNT(*)
|
|
271
|
-
FROM "
|
|
272
|
-
|
|
282
|
+
(SELECT COUNT(*)::int FROM "matched") AS "__total"
|
|
283
|
+
FROM (VALUES (1)) AS "one"
|
|
284
|
+
LEFT JOIN "page" ON true
|
|
273
285
|
ORDER BY ${orderSql}
|
|
274
|
-
OFFSET ${bind(skip)}::bigint
|
|
275
|
-
LIMIT ${bind(take)}::int
|
|
276
286
|
`;
|
|
277
|
-
const rows = await this.prisma.$queryRawUnsafe(
|
|
287
|
+
const rows = await this.prisma.$queryRawUnsafe(sql, ...params);
|
|
278
288
|
|
|
279
289
|
return {
|
|
280
|
-
mappings: rows
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}),
|
|
285
|
-
total: await this._totalMatches(rows, skip, whereSql, whereParams),
|
|
290
|
+
mappings: rows
|
|
291
|
+
.filter((row) => row.id !== null)
|
|
292
|
+
.map(({ __total, ...row }) => this._convertMappingIds(row)),
|
|
293
|
+
total: rows[0].__total,
|
|
286
294
|
};
|
|
287
295
|
}
|
|
288
296
|
|
|
289
|
-
/**
|
|
290
|
-
* An empty page is either past the end (count again) or, at skip 0,
|
|
291
|
-
* proof that nothing matches.
|
|
292
|
-
* @private
|
|
293
|
-
*/
|
|
294
|
-
async _totalMatches(pageRows, skip, whereSql, whereParams) {
|
|
295
|
-
if (pageRows.length > 0) return pageRows[0].__total;
|
|
296
|
-
if (skip === 0) return 0;
|
|
297
|
-
|
|
298
|
-
const [{ total }] = await this.prisma.$queryRawUnsafe(
|
|
299
|
-
`SELECT COUNT(*)::int AS "total" FROM "IntegrationMapping" WHERE ${whereSql}`,
|
|
300
|
-
...whereParams
|
|
301
|
-
);
|
|
302
|
-
return total;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* queryMappings reads the stored JSON, so it needs `mapping`, and every
|
|
307
|
-
* path inside it, written as plain JSON. The app's opt-out
|
|
308
|
-
* (`encryption.disable`) is registered lazily, when the Prisma client is
|
|
309
|
-
* created, so load it before deciding. The stage, the keys and the app
|
|
310
|
-
* definition do not change at runtime, so a passed check is kept.
|
|
311
|
-
* @private
|
|
312
|
-
*/
|
|
297
|
+
/** @private */
|
|
313
298
|
_assertMappingWrittenUnencrypted() {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.join(', ');
|
|
325
|
-
const optOut = fields.map((field) => `'${field}'`).join(', ');
|
|
326
|
-
throw new Error(
|
|
327
|
-
`queryMappings: field-level encryption still encrypts ${encrypted} on write, so it cannot be queried. Opt out by adding ${optOut} to appDefinition.encryption.disable.IntegrationMapping.`
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
this._mappingWrittenUnencrypted = true;
|
|
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
|
+
);
|
|
333
309
|
}
|
|
334
310
|
|
|
335
311
|
/**
|
|
@@ -349,30 +325,26 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
349
325
|
* treats JSON null as absent, and `notExists` is its exact negation.
|
|
350
326
|
* @private
|
|
351
327
|
*/
|
|
352
|
-
_conditionSql({
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return `(jsonb_typeof("mapping" #> ${path}) = 'string' AND "mapping" #>> ${path} = ANY(${values}::text[]))`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const type = `COALESCE(jsonb_typeof("mapping" #> ${path}), 'null')`;
|
|
365
|
-
return op === 'exists' ? `${type} <> 'null'` : `${type} = 'null'`;
|
|
328
|
+
_conditionSql({ field, path, op, value }, bind) {
|
|
329
|
+
const column = COLUMNS[field];
|
|
330
|
+
const operand = path
|
|
331
|
+
? jsonPathOperand(column, bind(path))
|
|
332
|
+
: { text: column };
|
|
333
|
+
return CONDITION_SQL[op]({
|
|
334
|
+
...operand,
|
|
335
|
+
value: value === undefined ? undefined : bind(value),
|
|
336
|
+
});
|
|
366
337
|
}
|
|
367
338
|
|
|
368
339
|
/**
|
|
369
340
|
* NULLIF folds JSON null into SQL NULL, so both sort after every value.
|
|
370
341
|
* @private
|
|
371
342
|
*/
|
|
372
|
-
_orderSql({
|
|
373
|
-
const
|
|
374
|
-
const value = `NULLIF(
|
|
375
|
-
|
|
343
|
+
_orderSql({ path, direction }, bind) {
|
|
344
|
+
const { json } = jsonPathOperand(COLUMNS.mapping, bind(path));
|
|
345
|
+
const value = `NULLIF(${json}, 'null'::jsonb)`;
|
|
346
|
+
const sqlDirection = SQL_DIRECTIONS[direction];
|
|
347
|
+
return `${value} ${sqlDirection} NULLS LAST, "id" ${sqlDirection}`;
|
|
376
348
|
}
|
|
377
349
|
|
|
378
350
|
/**
|
|
@@ -82,17 +82,6 @@ class IntegrationMappingRepository extends IntegrationMappingRepositoryInterface
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* Not implemented on this legacy repository; the adapters from
|
|
87
|
-
* createIntegrationMappingRepository() support it.
|
|
88
|
-
* @throws {Error} Always
|
|
89
|
-
*/
|
|
90
|
-
async queryMappings() {
|
|
91
|
-
throw new Error(
|
|
92
|
-
'queryMappings is not supported on the legacy IntegrationMappingRepository; use createIntegrationMappingRepository()'
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
85
|
/**
|
|
97
86
|
* Delete a mapping by integration and source ID
|
|
98
87
|
* Replaces: IntegrationMapping.deleteOne({ integration, sourceId })
|
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.bc0e60a.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.bc0e60a.0",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0--canary.651.bc0e60a.0",
|
|
53
|
+
"@friggframework/test": "2.0.0--canary.651.bc0e60a.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": "bc0e60a39a4ee32316d346a02ef8da77d4d41b1f"
|
|
94
94
|
}
|