@flowerforce/flowerbase 1.8.4-beta.4 → 1.8.4-beta.6

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.
Files changed (45) hide show
  1. package/README.md +3 -0
  2. package/dist/auth/plugins/jwt.d.ts +1 -1
  3. package/dist/auth/plugins/jwt.d.ts.map +1 -1
  4. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  5. package/dist/auth/providers/custom-function/controller.js +2 -1
  6. package/dist/cli/call-function.js +2 -1
  7. package/dist/features/functions/controller.d.ts.map +1 -1
  8. package/dist/features/functions/controller.js +31 -3
  9. package/dist/features/functions/dtos.d.ts +2 -0
  10. package/dist/features/functions/dtos.d.ts.map +1 -1
  11. package/dist/features/functions/interface.d.ts +2 -0
  12. package/dist/features/functions/interface.d.ts.map +1 -1
  13. package/dist/features/functions/utils.d.ts +3 -1
  14. package/dist/features/functions/utils.d.ts.map +1 -1
  15. package/dist/features/functions/utils.js +3 -1
  16. package/dist/services/index.d.ts +8 -8
  17. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  18. package/dist/services/mongodb-atlas/index.js +91 -20
  19. package/dist/services/mongodb-atlas/model.d.ts +2 -0
  20. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  21. package/dist/utils/context/helpers.d.ts +35 -32
  22. package/dist/utils/context/helpers.d.ts.map +1 -1
  23. package/dist/utils/context/helpers.js +19 -2
  24. package/dist/utils/context/interface.d.ts +1 -1
  25. package/dist/utils/context/interface.d.ts.map +1 -1
  26. package/dist/utils/roles/machines/read/D/validators.d.ts +1 -1
  27. package/dist/utils/roles/machines/read/D/validators.d.ts.map +1 -1
  28. package/dist/utils/roles/machines/write/C/validators.d.ts +1 -1
  29. package/dist/utils/roles/machines/write/C/validators.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/auth/providers/custom-function/controller.ts +2 -1
  32. package/src/cli/call-function.ts +2 -1
  33. package/src/features/functions/__tests__/controller.test.ts +42 -2
  34. package/src/features/functions/__tests__/utils.test.ts +44 -0
  35. package/src/features/functions/controller.ts +38 -2
  36. package/src/features/functions/dtos.ts +2 -0
  37. package/src/features/functions/interface.ts +2 -0
  38. package/src/features/functions/utils.ts +13 -0
  39. package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +116 -0
  40. package/src/services/mongodb-atlas/index.ts +122 -25
  41. package/src/services/mongodb-atlas/model.ts +8 -0
  42. package/src/utils/context/helpers.ts +18 -2
  43. package/src/utils/context/interface.ts +1 -1
  44. package/tsconfig.json +0 -5
  45. package/tsconfig.spec.json +2 -1
@@ -109,7 +109,7 @@ const createJwtUtils = () => {
109
109
  * @param functionsList -> the list of all functions
110
110
  */
111
111
  const generateContextData = ({ user, services, app, rules, currentFunction, functionName, functionsList, GenerateContextSync, request }) => {
112
- var _a;
112
+ var _a, _b, _c, _d, _e, _f;
113
113
  const BSON = mongodb_1.mongodb.BSON;
114
114
  const Binary = BSON === null || BSON === void 0 ? void 0 : BSON.Binary;
115
115
  if (Binary && typeof Binary.fromBase64 !== 'function') {
@@ -160,7 +160,24 @@ const generateContextData = ({ user, services, app, rules, currentFunction, func
160
160
  }
161
161
  },
162
162
  context: {
163
- request: Object.assign(Object.assign({}, request), { remoteIPAddress: request === null || request === void 0 ? void 0 : request.ip }),
163
+ request: {
164
+ remoteIPAddress: (_b = request === null || request === void 0 ? void 0 : request.ip) !== null && _b !== void 0 ? _b : '',
165
+ requestHeaders: (request === null || request === void 0 ? void 0 : request.headers)
166
+ ? Object.fromEntries(Object.entries(request.headers).map(([key, value]) => [
167
+ key,
168
+ Array.isArray(value) ? value : value !== undefined ? [value] : []
169
+ ]))
170
+ : {},
171
+ webhookUrl: (_c = request === null || request === void 0 ? void 0 : request.url) === null || _c === void 0 ? void 0 : _c.split('?')[0],
172
+ httpMethod: request === null || request === void 0 ? void 0 : request.method,
173
+ rawQueryString: ((_d = request === null || request === void 0 ? void 0 : request.url) === null || _d === void 0 ? void 0 : _d.includes('?'))
174
+ ? request.url.substring(request.url.indexOf('?'))
175
+ : '',
176
+ httpReferrer: (_e = request === null || request === void 0 ? void 0 : request.headers) === null || _e === void 0 ? void 0 : _e.referer,
177
+ httpUserAgent: (_f = request === null || request === void 0 ? void 0 : request.headers) === null || _f === void 0 ? void 0 : _f['user-agent'],
178
+ service: '',
179
+ action: ''
180
+ },
164
181
  user,
165
182
  environment: {
166
183
  tag: process.env.NODE_ENV
@@ -17,7 +17,7 @@ export interface GenerateContextParams {
17
17
  enqueue?: boolean;
18
18
  request?: ContextRequest;
19
19
  }
20
- type ContextRequest = Pick<FastifyRequest, "ips" | "host" | "hostname" | "url" | "method" | "ip" | "id">;
20
+ type ContextRequest = Pick<FastifyRequest, "ips" | "host" | "hostname" | "url" | "method" | "ip" | "id" | "headers">;
21
21
  export interface GenerateContextDataParams extends Omit<GenerateContextParams, 'args'> {
22
22
  GenerateContext: (params: GenerateContextParams) => Promise<unknown>;
23
23
  GenerateContextSync: (params: GenerateContextParams) => unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/utils/context/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEnD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,eAAe,EAAE,QAAQ,CAAA;IACzB,aAAa,EAAE,SAAS,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,KAAK,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,CAAA;AACxG,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACpE,mBAAmB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAA;CAChE"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/utils/context/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEnD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,eAAe,EAAE,QAAQ,CAAA;IACzB,aAAa,EAAE,SAAS,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,KAAK,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AACpH,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACpE,mBAAmB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAA;CAChE"}
@@ -1,4 +1,4 @@
1
1
  import { MachineContext } from '../../interface';
2
2
  export declare const checkAdditionalFieldsFn: ({ role }: MachineContext) => boolean;
3
- export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson/bson").Document>;
3
+ export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson").Document>;
4
4
  //# sourceMappingURL=validators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/read/D/validators.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,0CAOpE,CAAA"}
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/read/D/validators.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,qCAOpE,CAAA"}
@@ -1,4 +1,4 @@
1
1
  import { MachineContext } from '../../interface';
2
2
  export declare const checkAdditionalFieldsFn: ({ role }: MachineContext) => boolean;
3
- export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson/bson").Document>;
3
+ export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson").Document>;
4
4
  //# sourceMappingURL=validators.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/C/validators.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,0CAIpE,CAAA"}
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/C/validators.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,qCAIpE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.8.4-beta.4",
3
+ "version": "1.8.4-beta.6",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -75,7 +75,8 @@ export async function customFunctionController(app: FastifyInstance) {
75
75
  url,
76
76
  method,
77
77
  ip,
78
- id
78
+ id,
79
+ headers: req.headers
79
80
  }
80
81
  }) as CustomFunctionAuthResult
81
82
 
@@ -233,7 +233,8 @@ const executeLocal = async ({
233
233
  url: 'cli://local',
234
234
  host: 'cli',
235
235
  id: 'cli',
236
- hostname: 'cli'
236
+ hostname: 'cli',
237
+ headers: {}
237
238
  }
238
239
  })
239
240
  } finally {
@@ -1,4 +1,5 @@
1
1
  import Fastify, { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
2
+ import { services } from '../../../services'
2
3
  import { GenerateContext } from '../../../utils/context'
3
4
  import { functionsController } from '../controller'
4
5
 
@@ -8,18 +9,19 @@ jest.mock('../../../utils/context', () => ({
8
9
 
9
10
  describe('functionsController', () => {
10
11
  let app: FastifyInstance
12
+ const originalMongoService = services['mongodb-atlas']
11
13
 
12
14
  beforeEach(async () => {
13
15
  app = Fastify()
14
16
 
15
17
  app.decorate('jwtAuthentication', async (request: FastifyRequest, _reply: FastifyReply) => {
16
- ;(request as any).user = {
18
+ ; (request as any).user = {
17
19
  id: '507f191e810c19729de860ea',
18
20
  typ: 'access'
19
21
  }
20
22
  })
21
23
 
22
- ;(GenerateContext as jest.Mock).mockResolvedValue({ ok: true })
24
+ ; (GenerateContext as jest.Mock).mockResolvedValue({ ok: true })
23
25
 
24
26
  await app.register(functionsController, {
25
27
  functionsList: {
@@ -33,6 +35,7 @@ describe('functionsController', () => {
33
35
  })
34
36
 
35
37
  afterEach(async () => {
38
+ services['mongodb-atlas'] = originalMongoService
36
39
  await app.close()
37
40
  jest.clearAllMocks()
38
41
  })
@@ -57,4 +60,41 @@ describe('functionsController', () => {
57
60
  })
58
61
  )
59
62
  })
63
+
64
+ it('passes mongodb-atlas distinct service arguments through POST /call', async () => {
65
+ const distinct = jest.fn().mockResolvedValue(['open'])
66
+ services['mongodb-atlas'] = jest.fn(() => ({
67
+ db: jest.fn().mockReturnValue({
68
+ collection: jest.fn().mockReturnValue({
69
+ distinct
70
+ })
71
+ })
72
+ })) as any
73
+
74
+ const response = await app.inject({
75
+ method: 'POST',
76
+ url: '/call',
77
+ payload: {
78
+ service: 'mongodb-atlas',
79
+ name: 'distinct',
80
+ arguments: [
81
+ {
82
+ database: 'app',
83
+ collection: 'todos',
84
+ key: 'status',
85
+ query: { archived: false },
86
+ options: { maxTimeMS: 250 }
87
+ }
88
+ ]
89
+ }
90
+ })
91
+
92
+ expect(response.statusCode).toBe(200)
93
+ expect(JSON.parse(response.body)).toEqual(['open'])
94
+ expect(distinct).toHaveBeenCalledWith(
95
+ 'status',
96
+ { archived: false },
97
+ { maxTimeMS: 250 }
98
+ )
99
+ })
60
100
  })
@@ -30,4 +30,48 @@ describe('executeQuery', () => {
30
30
  { upsert: true }
31
31
  )
32
32
  })
33
+
34
+ it('passes distinct arguments through to the service method', async () => {
35
+ const currentMethod = jest.fn().mockResolvedValue(['open'])
36
+
37
+ const operators = await executeQuery({
38
+ currentMethod,
39
+ key: 'status',
40
+ query: { archived: false },
41
+ update: {},
42
+ options: { maxTimeMS: 500 }
43
+ } as any)
44
+
45
+ await operators.distinct()
46
+
47
+ expect(currentMethod).toHaveBeenCalledWith(
48
+ 'status',
49
+ { archived: false },
50
+ { maxTimeMS: 500 }
51
+ )
52
+ })
53
+
54
+ it('passes bulkWrite operations through to the service method', async () => {
55
+ const currentMethod = jest.fn().mockResolvedValue({ acknowledged: true })
56
+ const operations = [
57
+ {
58
+ updateOne: {
59
+ filter: { archived: false },
60
+ update: { $set: { archived: true } }
61
+ }
62
+ }
63
+ ]
64
+
65
+ const operators = await executeQuery({
66
+ currentMethod,
67
+ query: {},
68
+ update: {},
69
+ operations,
70
+ options: { ordered: false }
71
+ } as any)
72
+
73
+ await operators.bulkWrite()
74
+
75
+ expect(currentMethod).toHaveBeenCalledWith(operations, { ordered: false })
76
+ })
33
77
  })
@@ -9,6 +9,29 @@ import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
9
9
  import { FunctionController } from './interface'
10
10
  import { executeQuery } from './utils'
11
11
 
12
+ const SUPPORTED_QUERY_METHODS = [
13
+ 'find',
14
+ 'findOne',
15
+ 'count',
16
+ 'countDocuments',
17
+ 'distinct',
18
+ 'deleteOne',
19
+ 'insertOne',
20
+ 'updateOne',
21
+ 'findOneAndUpdate',
22
+ 'aggregate',
23
+ 'insertMany',
24
+ 'bulkWrite',
25
+ 'updateMany',
26
+ 'deleteMany'
27
+ ] as const
28
+
29
+ type SupportedQueryMethod = (typeof SUPPORTED_QUERY_METHODS)[number]
30
+
31
+ const isSupportedQueryMethod = (value: unknown): value is SupportedQueryMethod =>
32
+ typeof value === 'string' &&
33
+ (SUPPORTED_QUERY_METHODS as readonly string[]).includes(value)
34
+
12
35
  const normalizeUser = (payload: Record<string, any> | undefined) => {
13
36
  if (!payload) return undefined
14
37
  const nestedUser =
@@ -352,6 +375,7 @@ export const functionsController: FunctionController = async (
352
375
  const [{
353
376
  database,
354
377
  collection,
378
+ key,
355
379
  query,
356
380
  filter,
357
381
  update,
@@ -360,9 +384,14 @@ export const functionsController: FunctionController = async (
360
384
  returnNewDocument,
361
385
  document,
362
386
  documents,
387
+ operations,
363
388
  pipeline = []
364
389
  }] = args
365
390
 
391
+ if (!isSupportedQueryMethod(method)) {
392
+ throw new Error(`Unsupported service method "${String(method)}"`)
393
+ }
394
+
366
395
  const currentMethod = serviceFn(app, { rules, user })
367
396
  .db(database)
368
397
  .collection(collection)[method]
@@ -371,6 +400,7 @@ export const functionsController: FunctionController = async (
371
400
  const operatorsByType = await executeQuery({
372
401
  currentMethod,
373
402
  query,
403
+ key,
374
404
  filter,
375
405
  update,
376
406
  projection,
@@ -378,10 +408,15 @@ export const functionsController: FunctionController = async (
378
408
  returnNewDocument,
379
409
  document,
380
410
  documents,
411
+ operations,
381
412
  pipeline,
382
413
  isClient: true
383
414
  })
384
- const serviceResult = await operatorsByType[method as keyof typeof operatorsByType]()
415
+ const operator = operatorsByType[method]
416
+ if (typeof operator !== 'function') {
417
+ throw new Error(`Unsupported service method "${method}"`)
418
+ }
419
+ const serviceResult = await operator()
385
420
  res.type('application/json')
386
421
  return serializeEjson(serviceResult)
387
422
  }
@@ -406,7 +441,8 @@ export const functionsController: FunctionController = async (
406
441
  currentFunction,
407
442
  functionName: String(method),
408
443
  functionsList,
409
- services
444
+ services,
445
+ request: req
410
446
  })
411
447
  const normalizedResult = await normalizeFunctionResult(result)
412
448
  if (isReturnedError(normalizedResult)) {
@@ -23,6 +23,7 @@ export type FunctionCallBase64Dto = {
23
23
  type ArgumentsData = Arguments<{
24
24
  database: string
25
25
  collection: string
26
+ key?: string
26
27
  filter?: Document
27
28
  query: Parameters<GetOperatorsFunction>
28
29
  update: Document
@@ -31,6 +32,7 @@ type ArgumentsData = Arguments<{
31
32
  returnNewDocument?: boolean
32
33
  document: Document
33
34
  documents: Document[]
35
+ operations?: Document[]
34
36
  pipeline?: Document[]
35
37
  }>
36
38
 
@@ -27,12 +27,14 @@ export type ExecuteQueryParams = {
27
27
  currentMethod: ReturnType<GetOperatorsFunction>[keyof ReturnType<GetOperatorsFunction>]
28
28
  query: Parameters<GetOperatorsFunction>
29
29
  update: Document
30
+ key?: string
30
31
  filter?: Document
31
32
  projection?: Document
32
33
  options?: Document
33
34
  returnNewDocument?: boolean
34
35
  document: Document
35
36
  documents: Document[]
37
+ operations?: Document[]
36
38
  pipeline: Document[]
37
39
  isClient?: boolean
38
40
  }
@@ -45,12 +45,14 @@ export const executeQuery = async ({
45
45
  currentMethod,
46
46
  query,
47
47
  update,
48
+ key,
48
49
  filter,
49
50
  projection,
50
51
  options,
51
52
  returnNewDocument,
52
53
  document,
53
54
  documents,
55
+ operations,
54
56
  pipeline,
55
57
  isClient = false
56
58
  }: ExecuteQueryParams) => {
@@ -113,6 +115,12 @@ export const executeQuery = async ({
113
115
  EJSON.deserialize(resolvedQuery),
114
116
  parsedOptions
115
117
  ),
118
+ distinct: () =>
119
+ (currentMethod as ReturnType<GetOperatorsFunction>['distinct'])(
120
+ key ?? '',
121
+ EJSON.deserialize(resolvedQuery),
122
+ parsedOptions
123
+ ),
116
124
  deleteOne: () =>
117
125
  (currentMethod as ReturnType<GetOperatorsFunction>['deleteOne'])(
118
126
  EJSON.deserialize(resolvedQuery),
@@ -144,6 +152,11 @@ export const executeQuery = async ({
144
152
  (currentMethod as ReturnType<GetOperatorsFunction>['insertMany'])(
145
153
  EJSON.deserialize(documents)
146
154
  ),
155
+ bulkWrite: () =>
156
+ (currentMethod as ReturnType<GetOperatorsFunction>['bulkWrite'])(
157
+ EJSON.deserialize(operations ?? []),
158
+ parsedOptions
159
+ ),
147
160
  updateMany: () =>
148
161
  (currentMethod as ReturnType<GetOperatorsFunction>['updateMany'])(
149
162
  EJSON.deserialize(resolvedQuery),
@@ -1,3 +1,5 @@
1
+ /// <reference types="jest" />
2
+
1
3
  import { ObjectId } from 'mongodb'
2
4
  import MongoDbAtlas from '..'
3
5
  import { Rules } from '../../../features/rules/interface'
@@ -178,6 +180,70 @@ describe('mongodb-atlas Realm compatibility', () => {
178
180
  )
179
181
  })
180
182
 
183
+ it('forwards bulkWrite to the underlying collection in system mode', async () => {
184
+ const operations = [
185
+ {
186
+ updateOne: {
187
+ filter: { done: false },
188
+ update: { $set: { done: true } }
189
+ }
190
+ }
191
+ ]
192
+ const bulkWriteResult = {
193
+ acknowledged: true,
194
+ matchedCount: 1,
195
+ modifiedCount: 1,
196
+ insertedCount: 0,
197
+ deletedCount: 0,
198
+ upsertedCount: 0,
199
+ insertedIds: {},
200
+ upsertedIds: {}
201
+ }
202
+ const bulkWrite = jest.fn().mockResolvedValue(bulkWriteResult)
203
+ const collection = {
204
+ collectionName: 'todos',
205
+ bulkWrite
206
+ }
207
+
208
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
209
+ run_as_system: true
210
+ }).db('db').collection('todos')
211
+
212
+ const result = await operators.bulkWrite(operations as any, { ordered: false } as any)
213
+
214
+ expect(result).toEqual(bulkWriteResult)
215
+ expect(bulkWrite).toHaveBeenCalledWith(operations, { ordered: false })
216
+ })
217
+
218
+ it('rejects bulkWrite outside run_as_system mode', async () => {
219
+ const bulkWrite = jest.fn()
220
+ const collection = {
221
+ collectionName: 'todos',
222
+ bulkWrite
223
+ }
224
+
225
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
226
+ rules: createRules(),
227
+ user: { id: 'user-1' }
228
+ }).db('db').collection('todos')
229
+
230
+ await expect(
231
+ operators.bulkWrite(
232
+ [
233
+ {
234
+ updateOne: {
235
+ filter: { done: false },
236
+ update: { $set: { done: true } }
237
+ }
238
+ }
239
+ ] as any,
240
+ { ordered: false } as any
241
+ )
242
+ ).rejects.toThrow('bulkWrite is available only when run_as_system is enabled')
243
+
244
+ expect(bulkWrite).not.toHaveBeenCalled()
245
+ })
246
+
181
247
  it('supports operator updates in updateMany without using invalid aggregate stages', async () => {
182
248
  const id = new ObjectId()
183
249
  const find = jest.fn().mockReturnValue({
@@ -513,6 +579,56 @@ describe('mongodb-atlas Realm compatibility', () => {
513
579
  expect(result.insertedIds).toEqual([id0, id1])
514
580
  })
515
581
 
582
+ it('computes distinct values from readable documents only', async () => {
583
+ const find = jest.fn().mockReturnValue({
584
+ toArray: jest.fn().mockResolvedValue([
585
+ { _id: new ObjectId(), visible: 'A', secret: 'internal-1' },
586
+ { _id: new ObjectId(), visible: 'A', secret: 'internal-2' },
587
+ { _id: new ObjectId(), visible: 'B', secret: 'internal-1' }
588
+ ])
589
+ })
590
+ const collection = {
591
+ collectionName: 'todos',
592
+ find
593
+ }
594
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
595
+ rules: createRules({
596
+ roles: [
597
+ {
598
+ name: 'reader',
599
+ apply_when: {},
600
+ insert: true,
601
+ delete: true,
602
+ search: true,
603
+ read: true,
604
+ write: true,
605
+ fields: {
606
+ visible: { read: true },
607
+ secret: { read: false, write: false }
608
+ }
609
+ } as any
610
+ ]
611
+ }),
612
+ user: { id: 'user-1' }
613
+ }).db('db').collection('todos')
614
+
615
+ const visibleResult = await operators.distinct('visible', { archived: false })
616
+ const secretResult = await operators.distinct('secret', { archived: false })
617
+
618
+ expect(visibleResult).toEqual(['A', 'B'])
619
+ expect(secretResult).toEqual([])
620
+ expect(find).toHaveBeenNthCalledWith(
621
+ 1,
622
+ { $and: [{ archived: false }] },
623
+ { projection: { _id: 1, visible: 1 } }
624
+ )
625
+ expect(find).toHaveBeenNthCalledWith(
626
+ 2,
627
+ { $and: [{ archived: false }] },
628
+ { projection: { _id: 1, secret: 1 } }
629
+ )
630
+ })
631
+
516
632
  it('exposes startSession and delegates to the underlying MongoClient', async () => {
517
633
  const mockSession = { withTransaction: jest.fn() }
518
634
  const startSession = jest.fn().mockReturnValue(mockSession)