@flowerforce/flowerbase 1.7.6-beta.1 → 1.7.6-beta.3

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.
@@ -1,6 +1,8 @@
1
+ import type { Document } from 'mongodb';
1
2
  import { FunctionController } from './interface';
2
3
  export declare const mapWatchFilterToChangeStreamMatch: (value: unknown) => unknown;
3
4
  export declare const mapWatchFilterToDocumentQuery: (value: unknown) => unknown;
5
+ export declare const shouldSkipReadabilityLookupForChange: (change: Document) => boolean;
4
6
  /**
5
7
  * > Creates a pre handler for every query
6
8
  * @param app -> the fastify instance
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAmHhD,eAAO,MAAM,iCAAiC,GAAI,OAAO,OAAO,KAAG,OA0BlE,CAAA;AAID,eAAO,MAAM,6BAA6B,GAAI,OAAO,OAAO,KAAG,OAgD9D,CAAA;AAqHD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAoRjC,CAAA"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAIvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAmHhD,eAAO,MAAM,iCAAiC,GAAI,OAAO,OAAO,KAAG,OA0BlE,CAAA;AAID,eAAO,MAAM,6BAA6B,GAAI,OAAO,OAAO,KAAG,OAgD9D,CAAA;AAqHD,eAAO,MAAM,oCAAoC,GAAI,QAAQ,QAAQ,YAClC,CAAA;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAyRjC,CAAA"}
@@ -20,7 +20,7 @@ var __rest = (this && this.__rest) || function (s, e) {
20
20
  return t;
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.functionsController = exports.mapWatchFilterToDocumentQuery = exports.mapWatchFilterToChangeStreamMatch = void 0;
23
+ exports.functionsController = exports.shouldSkipReadabilityLookupForChange = exports.mapWatchFilterToDocumentQuery = exports.mapWatchFilterToChangeStreamMatch = void 0;
24
24
  const bson_1 = require("bson");
25
25
  const services_1 = require("../../services");
26
26
  const context_1 = require("../../utils/context");
@@ -262,6 +262,8 @@ const isReadableDocumentResult = (value) => !!value &&
262
262
  typeof value === 'object' &&
263
263
  !Array.isArray(value) &&
264
264
  Object.keys(value).length > 0;
265
+ const shouldSkipReadabilityLookupForChange = (change) => change.operationType === 'delete';
266
+ exports.shouldSkipReadabilityLookupForChange = shouldSkipReadabilityLookupForChange;
265
267
  /**
266
268
  * > Creates a pre handler for every query
267
269
  * @param app -> the fastify instance
@@ -431,6 +433,10 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
431
433
  const docId = (_b = (_a = change === null || change === void 0 ? void 0 : change.documentKey) === null || _a === void 0 ? void 0 : _a._id) !== null && _b !== void 0 ? _b : (_c = change === null || change === void 0 ? void 0 : change.fullDocument) === null || _c === void 0 ? void 0 : _c._id;
432
434
  if (typeof docId === 'undefined')
433
435
  return;
436
+ if ((0, exports.shouldSkipReadabilityLookupForChange)(change)) {
437
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`);
438
+ return;
439
+ }
434
440
  const readQuery = subscriber.documentFilter
435
441
  ? { $and: [subscriber.documentFilter, { _id: docId }] }
436
442
  : { _id: docId };
@@ -1,4 +1,7 @@
1
+ import { Document } from 'mongodb';
1
2
  import { MongodbAtlasFunction } from './model';
3
+ export declare const toWatchMatchFilter: (value: unknown) => unknown;
4
+ export declare const watchPipelineRequestsDelete: (pipeline: Document[]) => boolean;
2
5
  declare const MongodbAtlas: MongodbAtlasFunction;
3
6
  export default MongodbAtlas;
4
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAwBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AAq0ChB,QAAA,MAAM,YAAY,EAAE,oBAwBlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,QAAQ,EAQT,MAAM,SAAS,CAAA;AAOhB,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AAgJhB,eAAO,MAAM,kBAAkB,GAAI,OAAO,OAAO,KAAG,OA0BnD,CAAA;AA4BD,eAAO,MAAM,2BAA2B,GAAI,UAAU,QAAQ,EAAE,YAK5D,CAAA;AAgtCJ,QAAA,MAAM,YAAY,EAAE,oBAwBlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.watchPipelineRequestsDelete = exports.toWatchMatchFilter = void 0;
26
27
  const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
27
28
  const get_1 = __importDefault(require("lodash/get"));
28
29
  const isEqual_1 = __importDefault(require("lodash/isEqual"));
@@ -72,6 +73,12 @@ const findOptionKeys = new Set([
72
73
  'projection'
73
74
  ]);
74
75
  const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
76
+ const isTraversablePlainObject = (value) => {
77
+ if (!isPlainObject(value))
78
+ return false;
79
+ const prototype = Object.getPrototypeOf(value);
80
+ return prototype === Object.prototype || prototype === null;
81
+ };
75
82
  const looksLikeFindOptions = (value) => {
76
83
  if (!isPlainObject(value))
77
84
  return false;
@@ -114,21 +121,81 @@ const normalizeFindOneAndUpdateOptions = (options) => {
114
121
  return Object.assign(Object.assign({}, rest), { returnDocument: returnNewDocument ? 'after' : 'before' });
115
122
  };
116
123
  const buildAndQuery = (clauses) => clauses.length ? { $and: clauses } : {};
124
+ const watchChangeEventRootKeys = new Set([
125
+ '_id',
126
+ 'operationType',
127
+ 'clusterTime',
128
+ 'txnNumber',
129
+ 'lsid',
130
+ 'ns',
131
+ 'documentKey',
132
+ 'fullDocument',
133
+ 'updateDescription'
134
+ ]);
135
+ const isWatchChangeEventPath = (key) => {
136
+ if (watchChangeEventRootKeys.has(key))
137
+ return true;
138
+ return (key.startsWith('ns.') ||
139
+ key.startsWith('documentKey.') ||
140
+ key.startsWith('fullDocument.') ||
141
+ key.startsWith('updateDescription.'));
142
+ };
143
+ const isWatchOpaqueChangeEventObjectKey = (key) => key === 'ns' || key === 'documentKey' || key === 'fullDocument' || key === 'updateDescription';
117
144
  const toWatchMatchFilter = (value) => {
118
145
  if (Array.isArray(value)) {
119
- return value.map((item) => toWatchMatchFilter(item));
146
+ return value.map((item) => (0, exports.toWatchMatchFilter)(item));
120
147
  }
121
- if (!isPlainObject(value))
148
+ if (!isTraversablePlainObject(value))
122
149
  return value;
123
150
  return Object.entries(value).reduce((acc, [key, current]) => {
124
151
  if (key.startsWith('$')) {
125
- acc[key] = toWatchMatchFilter(current);
152
+ acc[key] = (0, exports.toWatchMatchFilter)(current);
126
153
  return acc;
127
154
  }
128
- acc[`fullDocument.${key}`] = toWatchMatchFilter(current);
155
+ if (isWatchOpaqueChangeEventObjectKey(key)) {
156
+ acc[key] = current;
157
+ return acc;
158
+ }
159
+ if (isWatchChangeEventPath(key)) {
160
+ acc[key] = (0, exports.toWatchMatchFilter)(current);
161
+ return acc;
162
+ }
163
+ acc[`fullDocument.${key}`] = (0, exports.toWatchMatchFilter)(current);
129
164
  return acc;
130
165
  }, {});
131
166
  };
167
+ exports.toWatchMatchFilter = toWatchMatchFilter;
168
+ const isDeleteOperationValue = (value) => {
169
+ if (typeof value === 'string')
170
+ return value.toLowerCase() === 'delete';
171
+ if (isPlainObject(value) && Array.isArray(value.$in)) {
172
+ return value.$in.some((entry) => isDeleteOperationValue(entry));
173
+ }
174
+ if (Array.isArray(value)) {
175
+ return value.some((entry) => isDeleteOperationValue(entry));
176
+ }
177
+ return false;
178
+ };
179
+ const hasDeleteOperationType = (value) => {
180
+ if (Array.isArray(value)) {
181
+ return value.some((entry) => hasDeleteOperationType(entry));
182
+ }
183
+ if (!isTraversablePlainObject(value))
184
+ return false;
185
+ return Object.entries(value).some(([key, current]) => {
186
+ if (key === 'operationType') {
187
+ return isDeleteOperationValue(current);
188
+ }
189
+ return hasDeleteOperationType(current);
190
+ });
191
+ };
192
+ const watchPipelineRequestsDelete = (pipeline) => pipeline.some((stage) => {
193
+ if (!isTraversablePlainObject(stage))
194
+ return false;
195
+ const match = stage.$match;
196
+ return hasDeleteOperationType(match);
197
+ });
198
+ exports.watchPipelineRequestsDelete = watchPipelineRequestsDelete;
132
199
  const resolveWatchArgs = (pipelineOrOptions, options) => {
133
200
  var _a;
134
201
  const inputPipeline = Array.isArray(pipelineOrOptions) ? pipelineOrOptions : [];
@@ -143,7 +210,7 @@ const resolveWatchArgs = (pipelineOrOptions, options) => {
143
210
  const _b = rawOptions, { filter: watchFilter, ids } = _b, watchOptions = __rest(_b, ["filter", "ids"]);
144
211
  const extraMatches = [];
145
212
  if (typeof watchFilter !== 'undefined') {
146
- extraMatches.push({ $match: toWatchMatchFilter(watchFilter) });
213
+ extraMatches.push({ $match: (0, exports.toWatchMatchFilter)(watchFilter) });
147
214
  }
148
215
  if (Array.isArray(ids)) {
149
216
  extraMatches.push({
@@ -861,15 +928,26 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
861
928
  (0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
862
929
  // Apply access filters to initial change stream pipeline
863
930
  const formattedQuery = (0, utils_3.getFormattedQuery)(filters, {}, user);
864
- const watchFormattedQuery = formattedQuery.map((condition) => toWatchMatchFilter(condition));
931
+ const watchFormattedQuery = formattedQuery.map((condition) => (0, exports.toWatchMatchFilter)(condition));
932
+ const requestedPipeline = [...extraMatches, ...pipeline];
933
+ const allowDeleteBypass = (0, exports.watchPipelineRequestsDelete)(requestedPipeline);
865
934
  const firstStep = watchFormattedQuery.length
866
935
  ? {
867
- $match: {
868
- $and: watchFormattedQuery
869
- }
936
+ $match: allowDeleteBypass
937
+ ? {
938
+ $or: [
939
+ {
940
+ $and: watchFormattedQuery
941
+ },
942
+ { operationType: 'delete' }
943
+ ]
944
+ }
945
+ : {
946
+ $and: watchFormattedQuery
947
+ }
870
948
  }
871
949
  : undefined;
872
- const formattedPipeline = [firstStep, ...extraMatches, ...pipeline].filter(Boolean);
950
+ const formattedPipeline = [firstStep, ...requestedPipeline].filter(Boolean);
873
951
  const result = changestreamCollection.watch(formattedPipeline, watchOptions);
874
952
  const originalOn = result.on.bind(result);
875
953
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.6-beta.1",
3
+ "version": "1.7.6-beta.3",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,5 +1,9 @@
1
1
  import { ObjectId } from 'mongodb'
2
- import { mapWatchFilterToChangeStreamMatch, mapWatchFilterToDocumentQuery } from '../controller'
2
+ import {
3
+ mapWatchFilterToChangeStreamMatch,
4
+ mapWatchFilterToDocumentQuery,
5
+ shouldSkipReadabilityLookupForChange
6
+ } from '../controller'
3
7
 
4
8
  describe('watch filter mapping', () => {
5
9
  it('keeps change-event fields untouched and prefixes only document fields', () => {
@@ -113,4 +117,10 @@ describe('watch filter mapping', () => {
113
117
  expect(documentQuery._id).toEqual(id)
114
118
  expect(documentQuery.operationType).toBeUndefined()
115
119
  })
120
+
121
+ it('skips readability lookup only for delete change events', () => {
122
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'delete' } as any)).toBe(true)
123
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'update' } as any)).toBe(false)
124
+ expect(shouldSkipReadabilityLookupForChange({ operationType: 'insert' } as any)).toBe(false)
125
+ })
116
126
  })
@@ -315,6 +315,9 @@ const isReadableDocumentResult = (value: unknown) =>
315
315
  !Array.isArray(value) &&
316
316
  Object.keys(value as Record<string, unknown>).length > 0
317
317
 
318
+ export const shouldSkipReadabilityLookupForChange = (change: Document) =>
319
+ change.operationType === 'delete'
320
+
318
321
  /**
319
322
  * > Creates a pre handler for every query
320
323
  * @param app -> the fastify instance
@@ -524,6 +527,11 @@ export const functionsController: FunctionController = async (
524
527
  (change as { fullDocument?: { _id?: unknown } })?.fullDocument?._id
525
528
  if (typeof docId === 'undefined') return
526
529
 
530
+ if (shouldSkipReadabilityLookupForChange(change)) {
531
+ subscriberRes.write(`data: ${serializeEjson(change)}\n\n`)
532
+ return
533
+ }
534
+
527
535
  const readQuery = subscriber.documentFilter
528
536
  ? ({ $and: [subscriber.documentFilter, { _id: docId }] } as Document)
529
537
  : ({ _id: docId } as Document)
@@ -26,7 +26,7 @@
26
26
  wsStatus,
27
27
  clearEvents
28
28
  } = dom;
29
- const { formatDateTime, highlightJson } = utils;
29
+ const { formatDateTime, renderJsonViewer, clearJsonViewer } = utils;
30
30
  const { setActiveTab } = helpers;
31
31
 
32
32
  const isOptionsEvent = (event) => {
@@ -0,0 +1,78 @@
1
+ import { ObjectId } from 'mongodb'
2
+ import { toWatchMatchFilter, watchPipelineRequestsDelete } from '../index'
3
+
4
+ describe('mongodb-atlas watch filter mapping', () => {
5
+ it('keeps change-event keys untouched and prefixes only document keys', () => {
6
+ const input = {
7
+ accountId: '699efbc09729e3b79f79e9b4',
8
+ operationType: 'delete',
9
+ $or: [
10
+ { requestId: '69a282a75cd849c244e001ca' },
11
+ { 'documentKey._id': '69a282a75cd849c244e001ca' }
12
+ ]
13
+ }
14
+
15
+ const output = toWatchMatchFilter(input)
16
+ expect(output).toEqual({
17
+ 'fullDocument.accountId': '699efbc09729e3b79f79e9b4',
18
+ operationType: 'delete',
19
+ $or: [
20
+ { 'fullDocument.requestId': '69a282a75cd849c244e001ca' },
21
+ { 'documentKey._id': '69a282a75cd849c244e001ca' }
22
+ ]
23
+ })
24
+
25
+ const outputJson = JSON.stringify(output)
26
+ expect(outputJson).not.toContain('fullDocument.operationType')
27
+ expect(outputJson).not.toContain('fullDocument.documentKey.')
28
+ })
29
+
30
+ it('preserves ObjectId values in watch filter mapping', () => {
31
+ const id = new ObjectId('69a282a75cd849c244e001ca')
32
+ const input = {
33
+ operationType: 'update',
34
+ 'documentKey._id': id,
35
+ requestId: id
36
+ }
37
+
38
+ const output = toWatchMatchFilter(input) as Record<string, unknown>
39
+ expect(output.operationType).toBe('update')
40
+ expect(output['documentKey._id']).toEqual(id)
41
+ expect(output['fullDocument.requestId']).toEqual(id)
42
+ })
43
+
44
+ it('detects delete operation requests in watch pipeline matches', () => {
45
+ expect(
46
+ watchPipelineRequestsDelete([
47
+ {
48
+ $match: {
49
+ $or: [
50
+ { operationType: 'insert' },
51
+ { operationType: 'delete' }
52
+ ]
53
+ }
54
+ }
55
+ ] as any)
56
+ ).toBe(true)
57
+
58
+ expect(
59
+ watchPipelineRequestsDelete([
60
+ {
61
+ $match: {
62
+ operationType: { $in: ['insert', 'replace'] }
63
+ }
64
+ }
65
+ ] as any)
66
+ ).toBe(false)
67
+
68
+ expect(
69
+ watchPipelineRequestsDelete([
70
+ {
71
+ $project: {
72
+ operationType: 1
73
+ }
74
+ }
75
+ ] as any)
76
+ ).toBe(false)
77
+ })
78
+ })
@@ -79,6 +79,12 @@ const findOptionKeys = new Set([
79
79
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
80
80
  !!value && typeof value === 'object' && !Array.isArray(value)
81
81
 
82
+ const isTraversablePlainObject = (value: unknown): value is Record<string, unknown> => {
83
+ if (!isPlainObject(value)) return false
84
+ const prototype = Object.getPrototypeOf(value)
85
+ return prototype === Object.prototype || prototype === null
86
+ }
87
+
82
88
  const looksLikeFindOptions = (value: unknown) => {
83
89
  if (!isPlainObject(value)) return false
84
90
  return Object.keys(value).some((key) => findOptionKeys.has(key))
@@ -140,23 +146,92 @@ const normalizeFindOneAndUpdateOptions = (
140
146
  const buildAndQuery = (clauses: MongoFilter<Document>[]): MongoFilter<Document> =>
141
147
  clauses.length ? { $and: clauses } : {}
142
148
 
143
- const toWatchMatchFilter = (value: unknown): unknown => {
149
+ const watchChangeEventRootKeys = new Set([
150
+ '_id',
151
+ 'operationType',
152
+ 'clusterTime',
153
+ 'txnNumber',
154
+ 'lsid',
155
+ 'ns',
156
+ 'documentKey',
157
+ 'fullDocument',
158
+ 'updateDescription'
159
+ ])
160
+
161
+ const isWatchChangeEventPath = (key: string) => {
162
+ if (watchChangeEventRootKeys.has(key)) return true
163
+ return (
164
+ key.startsWith('ns.') ||
165
+ key.startsWith('documentKey.') ||
166
+ key.startsWith('fullDocument.') ||
167
+ key.startsWith('updateDescription.')
168
+ )
169
+ }
170
+
171
+ const isWatchOpaqueChangeEventObjectKey = (key: string) =>
172
+ key === 'ns' || key === 'documentKey' || key === 'fullDocument' || key === 'updateDescription'
173
+
174
+ export const toWatchMatchFilter = (value: unknown): unknown => {
144
175
  if (Array.isArray(value)) {
145
176
  return value.map((item) => toWatchMatchFilter(item))
146
177
  }
147
178
 
148
- if (!isPlainObject(value)) return value
179
+ if (!isTraversablePlainObject(value)) return value
149
180
 
150
181
  return Object.entries(value).reduce<Record<string, unknown>>((acc, [key, current]) => {
151
182
  if (key.startsWith('$')) {
152
183
  acc[key] = toWatchMatchFilter(current)
153
184
  return acc
154
185
  }
186
+
187
+ if (isWatchOpaqueChangeEventObjectKey(key)) {
188
+ acc[key] = current
189
+ return acc
190
+ }
191
+
192
+ if (isWatchChangeEventPath(key)) {
193
+ acc[key] = toWatchMatchFilter(current)
194
+ return acc
195
+ }
196
+
155
197
  acc[`fullDocument.${key}`] = toWatchMatchFilter(current)
156
198
  return acc
157
199
  }, {})
158
200
  }
159
201
 
202
+ const isDeleteOperationValue = (value: unknown): boolean => {
203
+ if (typeof value === 'string') return value.toLowerCase() === 'delete'
204
+ if (isPlainObject(value) && Array.isArray((value as { $in?: unknown[] }).$in)) {
205
+ return (value as { $in: unknown[] }).$in.some((entry) => isDeleteOperationValue(entry))
206
+ }
207
+ if (Array.isArray(value)) {
208
+ return value.some((entry) => isDeleteOperationValue(entry))
209
+ }
210
+ return false
211
+ }
212
+
213
+ const hasDeleteOperationType = (value: unknown): boolean => {
214
+ if (Array.isArray(value)) {
215
+ return value.some((entry) => hasDeleteOperationType(entry))
216
+ }
217
+
218
+ if (!isTraversablePlainObject(value)) return false
219
+
220
+ return Object.entries(value).some(([key, current]) => {
221
+ if (key === 'operationType') {
222
+ return isDeleteOperationValue(current)
223
+ }
224
+ return hasDeleteOperationType(current)
225
+ })
226
+ }
227
+
228
+ export const watchPipelineRequestsDelete = (pipeline: Document[]) =>
229
+ pipeline.some((stage) => {
230
+ if (!isTraversablePlainObject(stage)) return false
231
+ const match = (stage as { $match?: unknown }).$match
232
+ return hasDeleteOperationType(match)
233
+ })
234
+
160
235
  type RealmCompatibleWatchOptions = Document & {
161
236
  filter?: MongoFilter<Document>
162
237
  ids?: unknown[]
@@ -1017,15 +1092,26 @@ const getOperators: GetOperatorsFunction = (
1017
1092
  (condition) => toWatchMatchFilter(condition) as MongoFilter<Document>
1018
1093
  )
1019
1094
 
1095
+ const requestedPipeline = [...extraMatches, ...pipeline]
1096
+ const allowDeleteBypass = watchPipelineRequestsDelete(requestedPipeline)
1020
1097
  const firstStep = watchFormattedQuery.length
1021
1098
  ? {
1022
- $match: {
1023
- $and: watchFormattedQuery
1024
- }
1099
+ $match: allowDeleteBypass
1100
+ ? {
1101
+ $or: [
1102
+ {
1103
+ $and: watchFormattedQuery
1104
+ },
1105
+ { operationType: 'delete' }
1106
+ ]
1107
+ }
1108
+ : {
1109
+ $and: watchFormattedQuery
1110
+ }
1025
1111
  }
1026
1112
  : undefined
1027
1113
 
1028
- const formattedPipeline = [firstStep, ...extraMatches, ...pipeline].filter(Boolean) as Document[]
1114
+ const formattedPipeline = [firstStep, ...requestedPipeline].filter(Boolean) as Document[]
1029
1115
 
1030
1116
  const result = changestreamCollection.watch(formattedPipeline, watchOptions)
1031
1117
  const originalOn = result.on.bind(result)