@flowerforce/flowerbase 1.7.2 → 1.7.3-beta.1
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/dist/features/functions/controller.js +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +247 -61
- package/dist/services/mongodb-atlas/model.d.ts +2 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/utils/context/helpers.d.ts +5 -2
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/context/helpers.js +2 -2
- package/dist/utils/context/index.d.ts +1 -0
- package/dist/utils/context/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +62 -26
- package/dist/utils/context/interface.d.ts +1 -0
- package/dist/utils/context/interface.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/functions/controller.ts +2 -2
- package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +0 -1
- package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +274 -0
- package/src/services/mongodb-atlas/index.ts +281 -65
- package/src/services/mongodb-atlas/model.ts +3 -0
- package/src/utils/__tests__/contextExecuteCompatibility.test.ts +60 -0
- package/src/utils/__tests__/generateContextData.test.ts +3 -1
- package/src/utils/context/helpers.ts +2 -1
- package/src/utils/context/index.ts +94 -47
- package/src/utils/context/interface.ts +1 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import get from 'lodash/get'
|
|
2
2
|
import isEqual from 'lodash/isEqual'
|
|
3
|
+
import set from 'lodash/set'
|
|
4
|
+
import unset from 'lodash/unset'
|
|
5
|
+
import cloneDeep from 'lodash/cloneDeep'
|
|
3
6
|
import {
|
|
7
|
+
ClientSession,
|
|
8
|
+
ClientSessionOptions,
|
|
4
9
|
Collection,
|
|
5
10
|
Document,
|
|
6
11
|
EventsDescription,
|
|
12
|
+
FindOneOptions,
|
|
13
|
+
FindOptions,
|
|
7
14
|
FindOneAndUpdateOptions,
|
|
8
15
|
Filter as MongoFilter,
|
|
9
16
|
UpdateFilter,
|
|
@@ -45,6 +52,222 @@ const logService = (message: string, payload?: unknown) => {
|
|
|
45
52
|
console.log('[service-debug]', message, payload ?? '')
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
const findOptionKeys = new Set([
|
|
56
|
+
'sort',
|
|
57
|
+
'skip',
|
|
58
|
+
'limit',
|
|
59
|
+
'session',
|
|
60
|
+
'hint',
|
|
61
|
+
'maxTimeMS',
|
|
62
|
+
'collation',
|
|
63
|
+
'allowPartialResults',
|
|
64
|
+
'noCursorTimeout',
|
|
65
|
+
'batchSize',
|
|
66
|
+
'returnKey',
|
|
67
|
+
'showRecordId',
|
|
68
|
+
'comment',
|
|
69
|
+
'let',
|
|
70
|
+
'projection'
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
74
|
+
!!value && typeof value === 'object' && !Array.isArray(value)
|
|
75
|
+
|
|
76
|
+
const looksLikeFindOptions = (value: unknown) => {
|
|
77
|
+
if (!isPlainObject(value)) return false
|
|
78
|
+
return Object.keys(value).some((key) => findOptionKeys.has(key))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolveFindArgs = (
|
|
82
|
+
projectionOrOptions?: Document | FindOptions | FindOneOptions,
|
|
83
|
+
options?: FindOptions | FindOneOptions
|
|
84
|
+
) => {
|
|
85
|
+
if (typeof options !== 'undefined') {
|
|
86
|
+
return {
|
|
87
|
+
projection: projectionOrOptions as Document | undefined,
|
|
88
|
+
options
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (looksLikeFindOptions(projectionOrOptions)) {
|
|
93
|
+
const resolvedOptions = projectionOrOptions as FindOptions | FindOneOptions
|
|
94
|
+
const projection =
|
|
95
|
+
isPlainObject(resolvedOptions) && isPlainObject(resolvedOptions.projection)
|
|
96
|
+
? (resolvedOptions.projection as Document)
|
|
97
|
+
: undefined
|
|
98
|
+
return {
|
|
99
|
+
projection,
|
|
100
|
+
options: resolvedOptions
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
projection: projectionOrOptions as Document | undefined,
|
|
106
|
+
options: undefined
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const normalizeInsertManyResult = <T extends { insertedIds?: Record<string, unknown> }>(result: T) => {
|
|
111
|
+
if (!result?.insertedIds || Array.isArray(result.insertedIds)) return result
|
|
112
|
+
return {
|
|
113
|
+
...result,
|
|
114
|
+
insertedIds: Object.values(result.insertedIds)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const hasAtomicOperators = (data: Document) => Object.keys(data).some((key) => key.startsWith('$'))
|
|
119
|
+
|
|
120
|
+
const normalizeUpdatePayload = (data: Document) =>
|
|
121
|
+
hasAtomicOperators(data) ? data : { $set: data }
|
|
122
|
+
|
|
123
|
+
const hasOperatorExpressions = (value: unknown) =>
|
|
124
|
+
isPlainObject(value) && Object.keys(value).some((key) => key.startsWith('$'))
|
|
125
|
+
|
|
126
|
+
const matchesPullCondition = (item: unknown, operand: unknown) => {
|
|
127
|
+
if (!isPlainObject(operand)) return isEqual(item, operand)
|
|
128
|
+
if (hasOperatorExpressions(operand)) {
|
|
129
|
+
if (Array.isArray((operand as { $in?: unknown }).$in)) {
|
|
130
|
+
return ((operand as { $in: unknown[] }).$in).some((candidate) => isEqual(candidate, item))
|
|
131
|
+
}
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
return Object.entries(operand).every(([key, value]) => isEqual(get(item, key), value))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const applyDocumentUpdateOperators = (baseDocument: Document, update: Document): Document => {
|
|
138
|
+
const updated = cloneDeep(baseDocument)
|
|
139
|
+
|
|
140
|
+
for (const [operator, payload] of Object.entries(update)) {
|
|
141
|
+
if (!isPlainObject(payload)) continue
|
|
142
|
+
|
|
143
|
+
switch (operator) {
|
|
144
|
+
case '$set':
|
|
145
|
+
Object.entries(payload).forEach(([path, value]) => set(updated, path, value))
|
|
146
|
+
break
|
|
147
|
+
case '$unset':
|
|
148
|
+
Object.keys(payload).forEach((path) => {
|
|
149
|
+
unset(updated, path)
|
|
150
|
+
})
|
|
151
|
+
break
|
|
152
|
+
case '$inc':
|
|
153
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
154
|
+
const currentValue = get(updated, path)
|
|
155
|
+
const increment = typeof value === 'number' ? value : 0
|
|
156
|
+
if (typeof currentValue === 'undefined') {
|
|
157
|
+
set(updated, path, increment)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (typeof currentValue !== 'number') {
|
|
161
|
+
throw new Error(`Cannot apply $inc to a non-numeric value at path "${path}"`)
|
|
162
|
+
}
|
|
163
|
+
set(updated, path, currentValue + increment)
|
|
164
|
+
})
|
|
165
|
+
break
|
|
166
|
+
case '$push':
|
|
167
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
168
|
+
const currentValue = get(updated, path)
|
|
169
|
+
const targetArray = Array.isArray(currentValue) ? [...currentValue] : []
|
|
170
|
+
if (isPlainObject(value) && Array.isArray((value as { $each?: unknown[] }).$each)) {
|
|
171
|
+
targetArray.push(...((value as { $each: unknown[] }).$each))
|
|
172
|
+
} else {
|
|
173
|
+
targetArray.push(value)
|
|
174
|
+
}
|
|
175
|
+
set(updated, path, targetArray)
|
|
176
|
+
})
|
|
177
|
+
break
|
|
178
|
+
case '$addToSet':
|
|
179
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
180
|
+
const currentValue = get(updated, path)
|
|
181
|
+
const targetArray = Array.isArray(currentValue) ? [...currentValue] : []
|
|
182
|
+
const valuesToAdd =
|
|
183
|
+
isPlainObject(value) && Array.isArray((value as { $each?: unknown[] }).$each)
|
|
184
|
+
? (value as { $each: unknown[] }).$each
|
|
185
|
+
: [value]
|
|
186
|
+
valuesToAdd.forEach((entry) => {
|
|
187
|
+
if (!targetArray.some((existing) => isEqual(existing, entry))) {
|
|
188
|
+
targetArray.push(entry)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
set(updated, path, targetArray)
|
|
192
|
+
})
|
|
193
|
+
break
|
|
194
|
+
case '$pull':
|
|
195
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
196
|
+
const currentValue = get(updated, path)
|
|
197
|
+
if (!Array.isArray(currentValue)) return
|
|
198
|
+
const filtered = currentValue.filter((entry) => !matchesPullCondition(entry, value))
|
|
199
|
+
set(updated, path, filtered)
|
|
200
|
+
})
|
|
201
|
+
break
|
|
202
|
+
case '$pop':
|
|
203
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
204
|
+
const currentValue = get(updated, path)
|
|
205
|
+
if (!Array.isArray(currentValue) || !currentValue.length) return
|
|
206
|
+
const next = [...currentValue]
|
|
207
|
+
if (value === -1) {
|
|
208
|
+
next.shift()
|
|
209
|
+
} else {
|
|
210
|
+
next.pop()
|
|
211
|
+
}
|
|
212
|
+
set(updated, path, next)
|
|
213
|
+
})
|
|
214
|
+
break
|
|
215
|
+
case '$mul':
|
|
216
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
217
|
+
const currentValue = get(updated, path)
|
|
218
|
+
const factor = typeof value === 'number' ? value : 1
|
|
219
|
+
if (typeof currentValue === 'undefined') {
|
|
220
|
+
set(updated, path, 0)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (typeof currentValue !== 'number') {
|
|
224
|
+
throw new Error(`Cannot apply $mul to a non-numeric value at path "${path}"`)
|
|
225
|
+
}
|
|
226
|
+
set(updated, path, currentValue * factor)
|
|
227
|
+
})
|
|
228
|
+
break
|
|
229
|
+
case '$min':
|
|
230
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
231
|
+
const currentValue = get(updated, path)
|
|
232
|
+
const comparableCurrent = currentValue as any
|
|
233
|
+
const comparableValue = value as any
|
|
234
|
+
if (typeof currentValue === 'undefined' || comparableCurrent > comparableValue) {
|
|
235
|
+
set(updated, path, value)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
break
|
|
239
|
+
case '$max':
|
|
240
|
+
Object.entries(payload).forEach(([path, value]) => {
|
|
241
|
+
const currentValue = get(updated, path)
|
|
242
|
+
const comparableCurrent = currentValue as any
|
|
243
|
+
const comparableValue = value as any
|
|
244
|
+
if (typeof currentValue === 'undefined' || comparableCurrent < comparableValue) {
|
|
245
|
+
set(updated, path, value)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
break
|
|
249
|
+
case '$rename':
|
|
250
|
+
Object.entries(payload).forEach(([fromPath, toPath]) => {
|
|
251
|
+
if (typeof toPath !== 'string') return
|
|
252
|
+
const currentValue = get(updated, fromPath)
|
|
253
|
+
if (typeof currentValue === 'undefined') return
|
|
254
|
+
set(updated, toPath, currentValue)
|
|
255
|
+
unset(updated, fromPath)
|
|
256
|
+
})
|
|
257
|
+
break
|
|
258
|
+
case '$currentDate':
|
|
259
|
+
Object.keys(payload).forEach((path) => set(updated, path, new Date()))
|
|
260
|
+
break
|
|
261
|
+
case '$setOnInsert':
|
|
262
|
+
break
|
|
263
|
+
default:
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return updated
|
|
269
|
+
}
|
|
270
|
+
|
|
48
271
|
const getUpdatedPaths = (update: Document) => {
|
|
49
272
|
const entries = Object.entries(update ?? {})
|
|
50
273
|
const hasOperators = entries.some(([key]) => key.startsWith('$'))
|
|
@@ -133,12 +356,16 @@ const getOperators: GetOperatorsFunction = (
|
|
|
133
356
|
* - Validates the result using `checkValidation` to ensure read permission.
|
|
134
357
|
* - If validation fails, returns an empty object; otherwise returns the validated document.
|
|
135
358
|
*/
|
|
136
|
-
findOne: async (query = {},
|
|
359
|
+
findOne: async (query = {}, projectionOrOptions, options) => {
|
|
137
360
|
try {
|
|
361
|
+
const { projection, options: normalizedOptions } = resolveFindArgs(
|
|
362
|
+
projectionOrOptions,
|
|
363
|
+
options
|
|
364
|
+
)
|
|
138
365
|
const resolvedOptions =
|
|
139
|
-
projection ||
|
|
366
|
+
projection || normalizedOptions
|
|
140
367
|
? {
|
|
141
|
-
...(
|
|
368
|
+
...(normalizedOptions ?? {}),
|
|
142
369
|
...(projection ? { projection } : {})
|
|
143
370
|
}
|
|
144
371
|
: undefined
|
|
@@ -350,6 +577,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
350
577
|
*/
|
|
351
578
|
updateOne: async (query, data, options) => {
|
|
352
579
|
try {
|
|
580
|
+
const normalizedData = normalizeUpdatePayload(data as Document)
|
|
353
581
|
if (!run_as_system) {
|
|
354
582
|
|
|
355
583
|
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
@@ -364,31 +592,23 @@ const getOperators: GetOperatorsFunction = (
|
|
|
364
592
|
const result = await collection.findOne({ $and: safeQuery })
|
|
365
593
|
|
|
366
594
|
if (!result) {
|
|
595
|
+
if (options?.upsert) {
|
|
596
|
+
const upsertResult = await collection.updateOne(
|
|
597
|
+
{ $and: safeQuery },
|
|
598
|
+
normalizedData,
|
|
599
|
+
options
|
|
600
|
+
)
|
|
601
|
+
emitMongoEvent('updateOne')
|
|
602
|
+
return upsertResult
|
|
603
|
+
}
|
|
367
604
|
throw new Error('Update not permitted')
|
|
368
605
|
}
|
|
369
606
|
|
|
370
607
|
const winningRole = getWinningRole(result, user, roles)
|
|
371
608
|
|
|
372
609
|
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
// Flatten the update object to extract the actual fields being modified
|
|
377
|
-
// const docToCheck = hasOperators
|
|
378
|
-
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
379
|
-
// : data
|
|
380
|
-
const pipeline = [
|
|
381
|
-
{
|
|
382
|
-
$match: { $and: safeQuery }
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
$limit: 1
|
|
386
|
-
},
|
|
387
|
-
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
388
|
-
]
|
|
389
|
-
const [docToCheck] = hasOperators
|
|
390
|
-
? await collection.aggregate(pipeline).toArray()
|
|
391
|
-
: ([data] as [Document])
|
|
610
|
+
const updatedPaths = getUpdatedPaths(normalizedData)
|
|
611
|
+
const docToCheck = applyDocumentUpdateOperators(result, normalizedData)
|
|
392
612
|
// Validate update permissions
|
|
393
613
|
const { status, document } = winningRole
|
|
394
614
|
? await checkValidation(
|
|
@@ -408,11 +628,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
408
628
|
if (!status || !areDocumentsEqual) {
|
|
409
629
|
throw new Error('Update not permitted')
|
|
410
630
|
}
|
|
411
|
-
const res = await collection.updateOne({ $and: safeQuery },
|
|
631
|
+
const res = await collection.updateOne({ $and: safeQuery }, normalizedData, options)
|
|
412
632
|
emitMongoEvent('updateOne')
|
|
413
633
|
return res
|
|
414
634
|
}
|
|
415
|
-
const result = await collection.updateOne(query,
|
|
635
|
+
const result = await collection.updateOne(query, normalizedData, options)
|
|
416
636
|
emitMongoEvent('updateOne')
|
|
417
637
|
return result
|
|
418
638
|
} catch (error) {
|
|
@@ -450,20 +670,19 @@ const getOperators: GetOperatorsFunction = (
|
|
|
450
670
|
}
|
|
451
671
|
|
|
452
672
|
const winningRole = getWinningRole(result, user, roles)
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
: ([data] as [Document])
|
|
673
|
+
const normalizedData = Array.isArray(data)
|
|
674
|
+
? data
|
|
675
|
+
: normalizeUpdatePayload(data as Document)
|
|
676
|
+
const updatedPaths = Array.isArray(normalizedData)
|
|
677
|
+
? []
|
|
678
|
+
: getUpdatedPaths(normalizedData as Document)
|
|
679
|
+
const [docToCheck] = Array.isArray(normalizedData)
|
|
680
|
+
? await collection.aggregate([
|
|
681
|
+
{ $match: { $and: safeQuery } },
|
|
682
|
+
{ $limit: 1 },
|
|
683
|
+
...normalizedData
|
|
684
|
+
]).toArray()
|
|
685
|
+
: [applyDocumentUpdateOperators(result, normalizedData as Document)]
|
|
467
686
|
|
|
468
687
|
const { status, document } = winningRole
|
|
469
688
|
? await checkValidation(
|
|
@@ -484,8 +703,8 @@ const getOperators: GetOperatorsFunction = (
|
|
|
484
703
|
}
|
|
485
704
|
|
|
486
705
|
const updateResult = options
|
|
487
|
-
? await collection.findOneAndUpdate({ $and: safeQuery },
|
|
488
|
-
: await collection.findOneAndUpdate({ $and: safeQuery },
|
|
706
|
+
? await collection.findOneAndUpdate({ $and: safeQuery }, normalizedData, options)
|
|
707
|
+
: await collection.findOneAndUpdate({ $and: safeQuery }, normalizedData)
|
|
489
708
|
if (!updateResult) {
|
|
490
709
|
emitMongoEvent('findOneAndUpdate')
|
|
491
710
|
return updateResult
|
|
@@ -540,12 +759,16 @@ const getOperators: GetOperatorsFunction = (
|
|
|
540
759
|
*
|
|
541
760
|
* This ensures that both pre-query filtering and post-query validation are applied consistently.
|
|
542
761
|
*/
|
|
543
|
-
find: (query = {},
|
|
762
|
+
find: (query = {}, projectionOrOptions, options) => {
|
|
544
763
|
try {
|
|
764
|
+
const { projection, options: normalizedOptions } = resolveFindArgs(
|
|
765
|
+
projectionOrOptions,
|
|
766
|
+
options
|
|
767
|
+
)
|
|
545
768
|
const resolvedOptions =
|
|
546
|
-
projection ||
|
|
769
|
+
projection || normalizedOptions
|
|
547
770
|
? {
|
|
548
|
-
...(
|
|
771
|
+
...(normalizedOptions ?? {}),
|
|
549
772
|
...(projection ? { projection } : {})
|
|
550
773
|
}
|
|
551
774
|
: undefined
|
|
@@ -867,12 +1090,12 @@ const getOperators: GetOperatorsFunction = (
|
|
|
867
1090
|
|
|
868
1091
|
const result = await collection.insertMany(documents, options)
|
|
869
1092
|
emitMongoEvent('insertMany')
|
|
870
|
-
return result
|
|
1093
|
+
return normalizeInsertManyResult(result)
|
|
871
1094
|
}
|
|
872
1095
|
// If system mode is active, insert all documents without validation
|
|
873
1096
|
const result = await collection.insertMany(documents, options)
|
|
874
1097
|
emitMongoEvent('insertMany')
|
|
875
|
-
return result
|
|
1098
|
+
return normalizeInsertManyResult(result)
|
|
876
1099
|
} catch (error) {
|
|
877
1100
|
emitMongoEvent('insertMany', undefined, error)
|
|
878
1101
|
throw error
|
|
@@ -880,6 +1103,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
880
1103
|
},
|
|
881
1104
|
updateMany: async (query, data, options) => {
|
|
882
1105
|
try {
|
|
1106
|
+
const normalizedData = normalizeUpdatePayload(data as Document)
|
|
883
1107
|
if (!run_as_system) {
|
|
884
1108
|
checkDenyOperation(normalizedRules, collection.collectionName, CRUD_OPERATIONS.UPDATE)
|
|
885
1109
|
// Apply access control filters
|
|
@@ -893,24 +1117,10 @@ const getOperators: GetOperatorsFunction = (
|
|
|
893
1117
|
}
|
|
894
1118
|
|
|
895
1119
|
// Check if the update data contains MongoDB update operators (e.g., $set, $inc)
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
// const docToCheck = hasOperators
|
|
901
|
-
// ? Object.values(data).reduce((acc, operation) => ({ ...acc, ...operation }), {})
|
|
902
|
-
// : data
|
|
903
|
-
|
|
904
|
-
const pipeline = [
|
|
905
|
-
{
|
|
906
|
-
$match: { $and: formattedQuery }
|
|
907
|
-
},
|
|
908
|
-
...Object.entries(data).map(([key, value]) => ({ [key]: value }))
|
|
909
|
-
]
|
|
910
|
-
|
|
911
|
-
const docsToCheck = hasOperators
|
|
912
|
-
? await collection.aggregate(pipeline).toArray()
|
|
913
|
-
: result
|
|
1120
|
+
const updatedPaths = getUpdatedPaths(normalizedData)
|
|
1121
|
+
const docsToCheck = result.map((currentDoc) =>
|
|
1122
|
+
applyDocumentUpdateOperators(currentDoc, normalizedData)
|
|
1123
|
+
)
|
|
914
1124
|
|
|
915
1125
|
const filteredItems = await Promise.all(
|
|
916
1126
|
docsToCheck.map(async (currentDoc) => {
|
|
@@ -944,11 +1154,11 @@ const getOperators: GetOperatorsFunction = (
|
|
|
944
1154
|
throw new Error('Update not permitted')
|
|
945
1155
|
}
|
|
946
1156
|
|
|
947
|
-
const res = await collection.updateMany({ $and: formattedQuery },
|
|
1157
|
+
const res = await collection.updateMany({ $and: formattedQuery }, normalizedData, options)
|
|
948
1158
|
emitMongoEvent('updateMany')
|
|
949
1159
|
return res
|
|
950
1160
|
}
|
|
951
|
-
const result = await collection.updateMany(query,
|
|
1161
|
+
const result = await collection.updateMany(query, normalizedData, options)
|
|
952
1162
|
emitMongoEvent('updateMany')
|
|
953
1163
|
return result
|
|
954
1164
|
} catch (error) {
|
|
@@ -1040,6 +1250,12 @@ const MongodbAtlas: MongodbAtlasFunction = (
|
|
|
1040
1250
|
app,
|
|
1041
1251
|
{ rules, user, run_as_system, monitoring } = {}
|
|
1042
1252
|
) => ({
|
|
1253
|
+
startSession: (options?: ClientSessionOptions) => {
|
|
1254
|
+
const mongoClient = app.mongo.client as unknown as {
|
|
1255
|
+
startSession: (sessionOptions?: ClientSessionOptions) => ClientSession
|
|
1256
|
+
}
|
|
1257
|
+
return mongoClient.startSession(options)
|
|
1258
|
+
},
|
|
1043
1259
|
db: (dbName: string) => {
|
|
1044
1260
|
return {
|
|
1045
1261
|
collection: (collName: string) => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FastifyInstance } from 'fastify'
|
|
2
2
|
import {
|
|
3
|
+
ClientSession,
|
|
4
|
+
ClientSessionOptions,
|
|
3
5
|
Collection,
|
|
4
6
|
Document,
|
|
5
7
|
FindCursor,
|
|
@@ -31,6 +33,7 @@ export type MongodbAtlasFunction = (
|
|
|
31
33
|
db: (dbName: string) => {
|
|
32
34
|
collection: (collName: string) => ReturnType<GetOperatorsFunction>
|
|
33
35
|
}
|
|
36
|
+
startSession: (options?: ClientSessionOptions) => ClientSession
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export type GetValidRuleParams<T extends Role | Filter> = {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { GenerateContextSync } from '../context'
|
|
2
|
+
import { Functions } from '../../features/functions/interface'
|
|
3
|
+
|
|
4
|
+
const mockServices = {
|
|
5
|
+
api: jest.fn().mockReturnValue({}),
|
|
6
|
+
aws: jest.fn().mockReturnValue({}),
|
|
7
|
+
'mongodb-atlas': jest.fn().mockReturnValue({})
|
|
8
|
+
} as any
|
|
9
|
+
|
|
10
|
+
describe('context.functions.execute compatibility', () => {
|
|
11
|
+
it('returns direct value when target function is synchronous', () => {
|
|
12
|
+
const functionsList = {
|
|
13
|
+
caller: {
|
|
14
|
+
code: 'module.exports = function() { return context.functions.execute("syncTarget") }'
|
|
15
|
+
},
|
|
16
|
+
syncTarget: {
|
|
17
|
+
code: 'module.exports = function() { return { ok: true } }'
|
|
18
|
+
}
|
|
19
|
+
} as Functions
|
|
20
|
+
|
|
21
|
+
const result = GenerateContextSync({
|
|
22
|
+
args: [],
|
|
23
|
+
app: {} as any,
|
|
24
|
+
rules: {} as any,
|
|
25
|
+
user: {} as any,
|
|
26
|
+
currentFunction: functionsList.caller,
|
|
27
|
+
functionsList,
|
|
28
|
+
services: mockServices,
|
|
29
|
+
functionName: 'caller'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({ ok: true })
|
|
33
|
+
expect(result).not.toBeInstanceOf(Promise)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns Promise when target function is asynchronous', async () => {
|
|
37
|
+
const functionsList = {
|
|
38
|
+
caller: {
|
|
39
|
+
code: 'module.exports = function() { return context.functions.execute("asyncTarget") }'
|
|
40
|
+
},
|
|
41
|
+
asyncTarget: {
|
|
42
|
+
code: 'module.exports = async function() { return { ok: true } }'
|
|
43
|
+
}
|
|
44
|
+
} as Functions
|
|
45
|
+
|
|
46
|
+
const result = GenerateContextSync({
|
|
47
|
+
args: [],
|
|
48
|
+
app: {} as any,
|
|
49
|
+
rules: {} as any,
|
|
50
|
+
user: {} as any,
|
|
51
|
+
currentFunction: functionsList.caller,
|
|
52
|
+
functionsList,
|
|
53
|
+
services: mockServices,
|
|
54
|
+
functionName: 'caller'
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(result && typeof (result as Promise<unknown>).then).toBe('function')
|
|
58
|
+
await expect(result).resolves.toEqual({ ok: true })
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -25,6 +25,7 @@ const mockFunctions = {
|
|
|
25
25
|
|
|
26
26
|
const currentFunction = mockFunctions.test
|
|
27
27
|
const GenerateContextMock = jest.fn()
|
|
28
|
+
const GenerateContextSyncMock = jest.fn()
|
|
28
29
|
const mockUser = {} as User
|
|
29
30
|
const mockRules = {} as Rules
|
|
30
31
|
const mockEnv = {
|
|
@@ -52,6 +53,7 @@ describe('generateContextData', () => {
|
|
|
52
53
|
functionsList: mockFunctions,
|
|
53
54
|
currentFunction,
|
|
54
55
|
GenerateContext: GenerateContextMock,
|
|
56
|
+
GenerateContextSync: GenerateContextSyncMock,
|
|
55
57
|
user: mockUser,
|
|
56
58
|
rules: mockRules
|
|
57
59
|
})
|
|
@@ -77,7 +79,7 @@ describe('generateContextData', () => {
|
|
|
77
79
|
mockErrorLog.mockRestore()
|
|
78
80
|
|
|
79
81
|
context.functions.execute('test')
|
|
80
|
-
expect(
|
|
82
|
+
expect(GenerateContextSyncMock).toHaveBeenCalled()
|
|
81
83
|
|
|
82
84
|
const token = jwt.sign(
|
|
83
85
|
{ sub: 'user', role: 'admin' },
|
|
@@ -117,6 +117,7 @@ export const generateContextData = ({
|
|
|
117
117
|
functionName,
|
|
118
118
|
functionsList,
|
|
119
119
|
GenerateContext,
|
|
120
|
+
GenerateContextSync,
|
|
120
121
|
request
|
|
121
122
|
}: GenerateContextDataParams) => {
|
|
122
123
|
const BSON = mongodb.BSON
|
|
@@ -206,7 +207,7 @@ export const generateContextData = ({
|
|
|
206
207
|
functions: {
|
|
207
208
|
execute: (name: keyof typeof functionsList, ...args: Arguments) => {
|
|
208
209
|
const currentFunction = functionsList[name] as Function
|
|
209
|
-
return
|
|
210
|
+
return GenerateContextSync({
|
|
210
211
|
args,
|
|
211
212
|
app,
|
|
212
213
|
rules,
|