Skip to content

Commit 799689e

Browse files
fix(NODE-4069): remove 'default' from options for fullDocument field in change stream options (#3169)
1 parent d43bd10 commit 799689e

File tree

4 files changed

+126
-27
lines changed

4 files changed

+126
-27
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"check:socks5": "mocha --config test/manual/mocharc.json test/manual/socks5.test.ts",
123123
"check:csfle": "mocha --config test/mocha_mongodb.json test/integration/client-side-encryption",
124124
"check:snappy": "mocha test/unit/assorted/snappy.test.js",
125+
"fix:eslint": "npm run check:eslint -- --fix",
125126
"prepare": "node etc/prepare.js",
126127
"preview:docs": "ts-node etc/docs/preview.ts",
127128
"release": "standard-version -i HISTORY.md",

src/change_stream.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,20 @@ const kClosed = Symbol('closed');
4545
/** @internal */
4646
const kMode = Symbol('mode');
4747

48-
const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
49-
const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
50-
CHANGE_STREAM_OPTIONS
51-
);
48+
const CHANGE_STREAM_OPTIONS = [
49+
'resumeAfter',
50+
'startAfter',
51+
'startAtOperationTime',
52+
'fullDocument'
53+
] as const;
54+
55+
const CURSOR_OPTIONS = [
56+
'batchSize',
57+
'maxAwaitTimeMS',
58+
'collation',
59+
'readPreference',
60+
...CHANGE_STREAM_OPTIONS
61+
] as const;
5262

5363
const CHANGE_DOMAIN_TYPES = {
5464
COLLECTION: Symbol('Collection'),
@@ -68,6 +78,8 @@ export interface ResumeOptions {
6878
maxAwaitTimeMS?: number;
6979
collation?: CollationOptions;
7080
readPreference?: ReadPreference;
81+
resumeAfter?: ResumeToken;
82+
startAfter?: ResumeToken;
7183
}
7284

7385
/**
@@ -94,7 +106,7 @@ export interface PipeOptions {
94106
* @public
95107
*/
96108
export interface ChangeStreamOptions extends AggregateOptions {
97-
/** Allowed values: ‘default’, ‘updateLookup. When set to updateLookup, the change stream will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred. */
109+
/** Allowed values: 'updateLookup'. When set to 'updateLookup', the change stream will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred. */
98110
fullDocument?: string;
99111
/** The maximum amount of time for the server to wait on new documents to satisfy a change stream query. */
100112
maxAwaitTimeMS?: number;
@@ -446,22 +458,18 @@ export class ChangeStreamCursor<TSchema extends Document = Document> extends Abs
446458
}
447459

448460
get resumeOptions(): ResumeOptions {
449-
const result = {} as ResumeOptions;
450-
for (const optionName of CURSOR_OPTIONS) {
451-
if (Reflect.has(this.options, optionName)) {
452-
Reflect.set(result, optionName, Reflect.get(this.options, optionName));
453-
}
454-
}
461+
const result: ResumeOptions = applyKnownOptions(this.options, CURSOR_OPTIONS);
455462

456463
if (this.resumeToken || this.startAtOperationTime) {
457-
['resumeAfter', 'startAfter', 'startAtOperationTime'].forEach(key =>
458-
Reflect.deleteProperty(result, key)
459-
);
464+
for (const key of ['resumeAfter', 'startAfter', 'startAtOperationTime']) {
465+
Reflect.deleteProperty(result, key);
466+
}
460467

461468
if (this.resumeToken) {
462469
const resumeKey =
463470
this.options.startAfter && !this.hasReceived ? 'startAfter' : 'resumeAfter';
464-
Reflect.set(result, resumeKey, this.resumeToken);
471+
472+
result[resumeKey] = this.resumeToken;
465473
} else if (this.startAtOperationTime && maxWireVersion(this.server) >= 7) {
466474
result.startAtOperationTime = this.startAtOperationTime;
467475
}
@@ -568,25 +576,25 @@ function setIsIterator<TSchema>(changeStream: ChangeStream<TSchema>): void {
568576
}
569577
changeStream[kMode] = 'iterator';
570578
}
579+
571580
/**
572581
* Create a new change stream cursor based on self's configuration
573582
* @internal
574583
*/
575584
function createChangeStreamCursor<TSchema>(
576585
changeStream: ChangeStream<TSchema>,
577-
options: ChangeStreamOptions
586+
options: ChangeStreamOptions | ResumeOptions
578587
): ChangeStreamCursor<TSchema> {
579-
const changeStreamStageOptions: Document = { fullDocument: options.fullDocument || 'default' };
580-
applyKnownOptions(changeStreamStageOptions, options, CHANGE_STREAM_OPTIONS);
588+
const changeStreamStageOptions = applyKnownOptions(options, CHANGE_STREAM_OPTIONS);
581589
if (changeStream.type === CHANGE_DOMAIN_TYPES.CLUSTER) {
582590
changeStreamStageOptions.allChangesForCluster = true;
583591
}
584-
585592
const pipeline = [{ $changeStream: changeStreamStageOptions } as Document].concat(
586593
changeStream.pipeline
587594
);
588595

589-
const cursorOptions = applyKnownOptions({}, options, CURSOR_OPTIONS);
596+
const cursorOptions: ChangeStreamCursorOptions = applyKnownOptions(options, CURSOR_OPTIONS);
597+
590598
const changeStreamCursor = new ChangeStreamCursor<TSchema>(
591599
getTopology(changeStream.parent),
592600
changeStream.namespace,
@@ -605,16 +613,17 @@ function createChangeStreamCursor<TSchema>(
605613
return changeStreamCursor;
606614
}
607615

608-
function applyKnownOptions(target: Document, source: Document, optionNames: string[]) {
609-
optionNames.forEach(name => {
610-
if (source[name]) {
611-
target[name] = source[name];
616+
function applyKnownOptions(source: Document, options: ReadonlyArray<string>) {
617+
const result: Document = {};
618+
619+
for (const option of options) {
620+
if (source[option]) {
621+
result[option] = source[option];
612622
}
613-
});
623+
}
614624

615-
return target;
625+
return result;
616626
}
617-
618627
interface TopologyWaitOptions {
619628
start?: number;
620629
timeout?: number;

test/integration/change-streams/change_stream.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,86 @@ describe('Change Streams', function () {
187187
});
188188
afterEach(async () => await mock.cleanup());
189189

190+
context('ChangeStreamCursor options', function () {
191+
let client, db, collection;
192+
193+
beforeEach(async function () {
194+
client = await this.configuration.newClient().connect();
195+
db = client.db('db');
196+
collection = db.collection('collection');
197+
});
198+
199+
afterEach(async function () {
200+
await client.close();
201+
client = undefined;
202+
db = undefined;
203+
collection = undefined;
204+
});
205+
206+
context('fullDocument', () => {
207+
it('does not set fullDocument if no value is provided', function () {
208+
const changeStream = client.watch();
209+
210+
expect(changeStream).not.to.have.nested.property(
211+
'cursor.pipeline[0].$changeStream.fullDocument'
212+
);
213+
});
214+
215+
it('does not validate the value passed in for the `fullDocument` property', function () {
216+
const changeStream = client.watch([], { fullDocument: 'invalid value' });
217+
218+
expect(changeStream).to.have.nested.property(
219+
'cursor.pipeline[0].$changeStream.fullDocument',
220+
'invalid value'
221+
);
222+
});
223+
224+
it('assigns `fullDocument` to the correct value if it is passed as an option', function () {
225+
const changeStream = client.watch([], { fullDocument: 'updateLookup' });
226+
227+
expect(changeStream).to.have.nested.property(
228+
'cursor.pipeline[0].$changeStream.fullDocument',
229+
'updateLookup'
230+
);
231+
});
232+
});
233+
234+
context('allChangesForCluster', () => {
235+
it('assigns `allChangesForCluster` to `true` if the ChangeStream.type is Cluster', function () {
236+
const changeStream = client.watch();
237+
238+
expect(changeStream).to.have.nested.property(
239+
'cursor.pipeline[0].$changeStream.allChangesForCluster',
240+
true
241+
);
242+
});
243+
244+
it('does not assign `allChangesForCluster` if the ChangeStream.type is Db', function () {
245+
const changeStream = db.watch();
246+
247+
expect(changeStream).not.to.have.nested.property(
248+
'cursor.pipeline[0].$changeStream.allChangesForCluster'
249+
);
250+
});
251+
252+
it('does not assign `allChangesForCluster` if the ChangeStream.type is Collection', function () {
253+
const changeStream = collection.watch();
254+
255+
expect(changeStream).not.to.have.nested.property(
256+
'cursor.pipeline[0].$changeStream.allChangesForCluster'
257+
);
258+
});
259+
});
260+
261+
it('ignores any invalid option values', function () {
262+
const changeStream = collection.watch([], { invalidOption: true });
263+
264+
expect(changeStream).not.to.have.nested.property(
265+
'cursor.pipeline[0].$changeStream.invalidOption'
266+
);
267+
});
268+
});
269+
190270
it('should close the listeners after the cursor is closed', {
191271
metadata: { requires: { topology: 'replicaset', mongodb: '>=3.6' } },
192272

test/types/change_stream.test-d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { expectType } from 'tsd';
2+
3+
import type { ChangeStreamOptions } from '../../src';
4+
5+
declare const changeStreamOptions: ChangeStreamOptions;
6+
7+
// The change stream spec says that we cannot throw an error for invalid values to `fullDocument`
8+
// for future compatability. This means we must leave `fullDocument` as type string.
9+
expectType<string | undefined>(changeStreamOptions.fullDocument);

0 commit comments

Comments
 (0)