@friggframework/core 2.0.0--canary.636.7dc8dca.0 → 2.0.0--canary.651.87132bf.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/integrations/repositories/integration-mapping-query.js +176 -0
- package/integrations/repositories/integration-mapping-repository-documentdb.js +8 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +45 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +8 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +160 -1
- package/integrations/repositories/integration-mapping-repository.js +11 -0
- package/package.json +5 -5
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
const SEGMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
const MAX_TAKE = 500;
|
|
12
|
+
const MAX_IN_VALUES = 500;
|
|
13
|
+
const MAX_CONDITIONS = 20;
|
|
14
|
+
const SQL_DIRECTIONS = { asc: 'ASC', desc: 'DESC' };
|
|
15
|
+
|
|
16
|
+
const OPS_BY_FIELD = {
|
|
17
|
+
mapping: ['exists', 'notExists', 'in'],
|
|
18
|
+
sourceId: ['notStartsWith'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function validateMappingQuery(query) {
|
|
22
|
+
if (!isPlainObject(query)) {
|
|
23
|
+
throw new Error('queryMappings: query must be an object');
|
|
24
|
+
}
|
|
25
|
+
const { where = [], orderBy, skip = 0, take, omit = [] } = query;
|
|
26
|
+
if (!Array.isArray(where)) {
|
|
27
|
+
throw new Error('queryMappings: where must be an array');
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isInteger(take) || take < 1 || take > MAX_TAKE) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`queryMappings: take must be an integer between 1 and ${MAX_TAKE}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isSafeInteger(skip) || skip < 0) {
|
|
35
|
+
throw new Error('queryMappings: skip must be a non-negative integer');
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
!Array.isArray(omit) ||
|
|
39
|
+
!omit.every((key) => typeof key === 'string' && SEGMENT_REGEX.test(key))
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`queryMappings: omit must be an array of top-level mapping keys matching ${SEGMENT_REGEX}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const whereEntries = where.map(toWhereEntry);
|
|
47
|
+
const conditionCount = whereEntries.reduce(
|
|
48
|
+
(count, entry) => count + (entry.anyOf ? entry.anyOf.length : 1),
|
|
49
|
+
0
|
|
50
|
+
);
|
|
51
|
+
if (conditionCount > MAX_CONDITIONS) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`queryMappings: where must have at most ${MAX_CONDITIONS} conditions, anyOf members included`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
where: whereEntries,
|
|
59
|
+
orderBy: orderBy === undefined ? null : toOrderBy(orderBy),
|
|
60
|
+
skip,
|
|
61
|
+
take,
|
|
62
|
+
omit,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toOrderBy(orderBy) {
|
|
67
|
+
if (!isPlainObject(orderBy)) {
|
|
68
|
+
throw new Error('queryMappings: orderBy must be an object');
|
|
69
|
+
}
|
|
70
|
+
const { field, segments } = parsePath(orderBy.path);
|
|
71
|
+
if (field !== 'mapping') {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"queryMappings: orderBy.path must be a mapping path ('mapping.<segment>...')"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!Object.hasOwn(SQL_DIRECTIONS, orderBy.direction)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"queryMappings: orderBy.direction must be 'asc' or 'desc'"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return { segments, direction: SQL_DIRECTIONS[orderBy.direction] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toWhereEntry(entry) {
|
|
85
|
+
assertWhereEntryShape(entry);
|
|
86
|
+
if (!('anyOf' in entry)) return toCondition(entry);
|
|
87
|
+
|
|
88
|
+
const { anyOf } = entry;
|
|
89
|
+
if (!Array.isArray(anyOf) || anyOf.length === 0) {
|
|
90
|
+
throw new Error('queryMappings: anyOf must be a non-empty array');
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
anyOf: anyOf.map((condition) => {
|
|
94
|
+
assertWhereEntryShape(condition);
|
|
95
|
+
if ('anyOf' in condition) {
|
|
96
|
+
throw new Error('queryMappings: nested anyOf is not supported');
|
|
97
|
+
}
|
|
98
|
+
return toCondition(condition);
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertWhereEntryShape(entry) {
|
|
104
|
+
if (!isPlainObject(entry)) {
|
|
105
|
+
throw new Error('queryMappings: each where entry must be an object');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPlainObject(value) {
|
|
110
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toCondition({ path, op, value }) {
|
|
114
|
+
const { field, segments } = parsePath(path);
|
|
115
|
+
const allowed = OPS_BY_FIELD[field];
|
|
116
|
+
if (!allowed.includes(op)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`queryMappings: op ${JSON.stringify(
|
|
119
|
+
op
|
|
120
|
+
)} is not allowed on '${path}' (allowed: ${allowed.join(', ')})`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (op === 'in') {
|
|
125
|
+
if (
|
|
126
|
+
!Array.isArray(value) ||
|
|
127
|
+
value.length === 0 ||
|
|
128
|
+
!value.every((v) => typeof v === 'string')
|
|
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
|
+
}
|
|
141
|
+
|
|
142
|
+
if (op === 'notStartsWith') {
|
|
143
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`queryMappings: 'notStartsWith' value must be a non-empty string on '${path}'`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { field, segments, op, value };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { field, segments, op };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `'mapping.c2h.lastStatus'` → `{ field: 'mapping', segments: ['c2h', 'lastStatus'] }`,
|
|
156
|
+
* `'sourceId'` → `{ field: 'sourceId', segments: [] }`.
|
|
157
|
+
*/
|
|
158
|
+
function parsePath(path) {
|
|
159
|
+
const [field, ...segments] =
|
|
160
|
+
typeof path === 'string' ? path.split('.') : [];
|
|
161
|
+
const valid =
|
|
162
|
+
(field === 'sourceId' && segments.length === 0) ||
|
|
163
|
+
(field === 'mapping' &&
|
|
164
|
+
segments.length > 0 &&
|
|
165
|
+
segments.every((segment) => SEGMENT_REGEX.test(segment)));
|
|
166
|
+
if (!valid) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`queryMappings: invalid path ${JSON.stringify(
|
|
169
|
+
path
|
|
170
|
+
)} (must be 'sourceId' or 'mapping.<segment>...' with segments matching ${SEGMENT_REGEX})`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return { field, segments };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { SEGMENT_REGEX, validateMappingQuery };
|
|
@@ -179,6 +179,14 @@ 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
|
+
|
|
182
190
|
async deleteMapping(integrationId, sourceId) {
|
|
183
191
|
const filter = this._compositeFilter(integrationId, sourceId);
|
|
184
192
|
const result = await deleteOne(
|
|
@@ -52,6 +52,51 @@ class IntegrationMappingRepositoryInterface {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Query one filtered, ordered page of an integration's mappings without
|
|
57
|
+
* loading every row. Rows have the same shape as findMappingsByIntegration.
|
|
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
|
+
* Paths address the `mapping` JSON by identifier-only segments
|
|
65
|
+
* (`'mapping.c2h.lastStatus'`), or the `sourceId` column. Conditions:
|
|
66
|
+
* - `{ path: 'mapping.…', op: 'exists' | 'notExists' }` — JSON null counts
|
|
67
|
+
* as absent; notExists is the exact negation of exists.
|
|
68
|
+
* - `{ path: 'mapping.…', op: 'in', value: string[] }` — matches JSON
|
|
69
|
+
* strings; 1–500 values.
|
|
70
|
+
* - `{ path: 'sourceId', op: 'notStartsWith', value: string }` — a NULL
|
|
71
|
+
* sourceId matches.
|
|
72
|
+
*
|
|
73
|
+
* Only rows whose `mapping` is a JSON object can match, so rows whose
|
|
74
|
+
* whole `mapping` is still ciphertext from before an encryption opt-out
|
|
75
|
+
* never do. Adapters refuse to run while field-level encryption is enabled
|
|
76
|
+
* and still encrypts `mapping`, or a path inside it, on write; opt out with
|
|
77
|
+
* `appDefinition.encryption.disable = { IntegrationMapping: ['mapping'] }`
|
|
78
|
+
* plus any nested `mapping.…` path a custom schema encrypts.
|
|
79
|
+
*
|
|
80
|
+
* @param {string|number} integrationId - The integration ID
|
|
81
|
+
* @param {Object} query
|
|
82
|
+
* @param {Array<Object>} [query.where=[]] - Conditions ANDed together; an
|
|
83
|
+
* entry may be `{ anyOf: Condition[] }` (one level, ORed). At most 20
|
|
84
|
+
* conditions, anyOf members included.
|
|
85
|
+
* @param {{path: string, direction: 'asc'|'desc'}} [query.orderBy] - A
|
|
86
|
+
* mapping path; nulls last, ties broken by id in the same direction.
|
|
87
|
+
* Without it rows are ordered by id ascending.
|
|
88
|
+
* @param {number} [query.skip=0] - Rows to skip (integer ≥ 0)
|
|
89
|
+
* @param {number} query.take - Page size (integer 1–500)
|
|
90
|
+
* @param {string[]} [query.omit=[]] - Top-level mapping keys to leave out of
|
|
91
|
+
* the returned rows; such projected rows must not be written back
|
|
92
|
+
* @returns {Promise<{mappings: Array<Object>, total: number}>} The page, and
|
|
93
|
+
* the number of rows matching `where`
|
|
94
|
+
* @abstract
|
|
95
|
+
*/
|
|
96
|
+
async queryMappings(integrationId, query) {
|
|
97
|
+
throw new Error('Method queryMappings must be implemented by subclass');
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
/**
|
|
56
101
|
* Delete a specific mapping
|
|
57
102
|
*
|
|
@@ -87,6 +87,14 @@ 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
|
+
|
|
90
98
|
/**
|
|
91
99
|
* Delete a mapping by integration and source ID
|
|
92
100
|
* Replaces: IntegrationMapping.deleteOne({ integration, sourceId })
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
const { prisma } = require('../../database/prisma');
|
|
1
|
+
const { prisma, getEncryptionConfig } = require('../../database/prisma');
|
|
2
|
+
const {
|
|
3
|
+
getFieldsToEncryptOnWrite,
|
|
4
|
+
loadCustomEncryptionSchema,
|
|
5
|
+
} = require('../../database/encryption/encryption-schema-registry');
|
|
2
6
|
const {
|
|
3
7
|
IntegrationMappingRepositoryInterface,
|
|
4
8
|
} = require('./integration-mapping-repository-interface');
|
|
5
9
|
const { strictIntId } = require('./report-id');
|
|
10
|
+
const { validateMappingQuery } = require('./integration-mapping-query');
|
|
11
|
+
|
|
12
|
+
const encryptedMappingFields = () =>
|
|
13
|
+
getFieldsToEncryptOnWrite('IntegrationMapping').filter(
|
|
14
|
+
(field) => field === 'mapping' || field.startsWith('mapping.')
|
|
15
|
+
);
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* PostgreSQL Integration Mapping Repository Adapter
|
|
@@ -216,6 +226,155 @@ class IntegrationMappingRepositoryPostgres extends IntegrationMappingRepositoryI
|
|
|
216
226
|
return counts;
|
|
217
227
|
}
|
|
218
228
|
|
|
229
|
+
/**
|
|
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
|
+
* @param {string} integrationId
|
|
240
|
+
* @param {Object} query - See IntegrationMappingRepositoryInterface.queryMappings
|
|
241
|
+
* @returns {Promise<{mappings: Array<Object>, total: number}>}
|
|
242
|
+
*/
|
|
243
|
+
async queryMappings(integrationId, query) {
|
|
244
|
+
const { where, orderBy, skip, take, omit } =
|
|
245
|
+
validateMappingQuery(query);
|
|
246
|
+
const intIntegrationId = strictIntId(integrationId);
|
|
247
|
+
this._assertMappingWrittenUnencrypted();
|
|
248
|
+
|
|
249
|
+
const params = [];
|
|
250
|
+
const bind = (v) => {
|
|
251
|
+
params.push(v);
|
|
252
|
+
return `$${params.length}`;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const whereSql = [
|
|
256
|
+
`"integrationId" = ${bind(intIntegrationId)}::int`,
|
|
257
|
+
`jsonb_typeof("mapping") = 'object'`,
|
|
258
|
+
...where.map((entry) => this._whereEntrySql(entry, bind)),
|
|
259
|
+
].join(' AND ');
|
|
260
|
+
const whereParams = [...params];
|
|
261
|
+
|
|
262
|
+
const mappingSql =
|
|
263
|
+
omit.length > 0
|
|
264
|
+
? `"mapping" - ${bind(omit)}::text[] AS "mapping"`
|
|
265
|
+
: `"mapping"`;
|
|
266
|
+
const orderSql = orderBy ? this._orderSql(orderBy, bind) : `"id" ASC`;
|
|
267
|
+
|
|
268
|
+
const pageSql = `
|
|
269
|
+
SELECT "id", "integrationId", "sourceId", ${mappingSql}, "createdAt", "updatedAt",
|
|
270
|
+
(COUNT(*) OVER ())::int AS "__total"
|
|
271
|
+
FROM "IntegrationMapping"
|
|
272
|
+
WHERE ${whereSql}
|
|
273
|
+
ORDER BY ${orderSql}
|
|
274
|
+
OFFSET ${bind(skip)}::bigint
|
|
275
|
+
LIMIT ${bind(take)}::int
|
|
276
|
+
`;
|
|
277
|
+
const rows = await this.prisma.$queryRawUnsafe(pageSql, ...params);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
mappings: rows.map((row) => {
|
|
281
|
+
const mapping = this._convertMappingIds(row);
|
|
282
|
+
delete mapping.__total;
|
|
283
|
+
return mapping;
|
|
284
|
+
}),
|
|
285
|
+
total: await this._totalMatches(rows, skip, whereSql, whereParams),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
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
|
+
*/
|
|
313
|
+
_assertMappingWrittenUnencrypted() {
|
|
314
|
+
if (this._mappingWrittenUnencrypted) return;
|
|
315
|
+
|
|
316
|
+
if (getEncryptionConfig().enabled) {
|
|
317
|
+
if (encryptedMappingFields().length > 0) {
|
|
318
|
+
loadCustomEncryptionSchema();
|
|
319
|
+
}
|
|
320
|
+
const fields = encryptedMappingFields();
|
|
321
|
+
if (fields.length > 0) {
|
|
322
|
+
const encrypted = fields
|
|
323
|
+
.map((field) => `IntegrationMapping.${field}`)
|
|
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;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* One where entry: a condition, or an anyOf group as a parenthesized OR.
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
_whereEntrySql(entry, bind) {
|
|
340
|
+
if (!entry.anyOf) return this._conditionSql(entry, bind);
|
|
341
|
+
const alternatives = entry.anyOf.map((condition) =>
|
|
342
|
+
this._conditionSql(condition, bind)
|
|
343
|
+
);
|
|
344
|
+
return `(${alternatives.join(' OR ')})`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* SQL predicate for one validated queryMappings condition. `exists`
|
|
349
|
+
* treats JSON null as absent, and `notExists` is its exact negation.
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
_conditionSql({ segments, op, value }, bind) {
|
|
353
|
+
if (op === 'notStartsWith') {
|
|
354
|
+
const prefix = bind(value);
|
|
355
|
+
return `("sourceId" IS NULL OR NOT starts_with("sourceId", ${prefix}::text))`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const path = `${bind(segments)}::text[]`;
|
|
359
|
+
if (op === 'in') {
|
|
360
|
+
const values = bind(value);
|
|
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'`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* NULLIF folds JSON null into SQL NULL, so both sort after every value.
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
_orderSql({ segments, direction }, bind) {
|
|
373
|
+
const path = bind(segments);
|
|
374
|
+
const value = `NULLIF("mapping" #> ${path}::text[], 'null'::jsonb)`;
|
|
375
|
+
return `${value} ${direction} NULLS LAST, "id" ${direction}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
219
378
|
/**
|
|
220
379
|
* Find mapping by ID
|
|
221
380
|
* @param {string} id - Mapping ID (string from application layer)
|
|
@@ -82,6 +82,17 @@ 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
|
+
|
|
85
96
|
/**
|
|
86
97
|
* Delete a mapping by integration and source ID
|
|
87
98
|
* 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.
|
|
4
|
+
"version": "2.0.0--canary.651.87132bf.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.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
53
|
-
"@friggframework/test": "2.0.0--canary.
|
|
51
|
+
"@friggframework/eslint-config": "2.0.0--canary.651.87132bf.0",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0--canary.651.87132bf.0",
|
|
53
|
+
"@friggframework/test": "2.0.0--canary.651.87132bf.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": "87132bf8f734ff6733c582630a606170e86a5dc2"
|
|
94
94
|
}
|