Skip to content

Commit 4e2f9bf

Browse files
feat(NODE-3699): add support for comment field (#3167)
1 parent 43ba9fc commit 4e2f9bf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+6619
-344
lines changed

src/change_stream.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const CURSOR_OPTIONS = [
5757
'maxAwaitTimeMS',
5858
'collation',
5959
'readPreference',
60+
'comment',
6061
...CHANGE_STREAM_OPTIONS
6162
] as const;
6263

@@ -410,7 +411,7 @@ export class ChangeStream<TSchema extends Document = Document> extends TypedEven
410411
export interface ChangeStreamCursorOptions extends AbstractCursorOptions {
411412
startAtOperationTime?: OperationTime;
412413
resumeAfter?: ResumeToken;
413-
startAfter?: boolean;
414+
startAfter?: ResumeToken;
414415
}
415416

416417
/** @internal */
@@ -617,7 +618,7 @@ function applyKnownOptions(source: Document, options: ReadonlyArray<string>) {
617618
const result: Document = {};
618619

619620
for (const option of options) {
620-
if (source[option]) {
621+
if (option in source) {
621622
result[option] = source[option];
622623
}
623624
}

src/cmap/connection.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,15 @@ export interface GetMoreOptions extends CommandOptions {
124124
batchSize?: number;
125125
maxTimeMS?: number;
126126
maxAwaitTimeMS?: number;
127-
comment?: Document | string;
127+
/**
128+
* Comment to apply to the operation.
129+
*
130+
* In server versions pre-4.4, 'comment' must be string. A server
131+
* error will be thrown if any other type is provided.
132+
*
133+
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
134+
*/
135+
comment?: unknown;
128136
}
129137

130138
/** @public */
@@ -574,6 +582,11 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
574582
if (typeof options.maxAwaitTimeMS === 'number') {
575583
getMoreCmd.maxTimeMS = options.maxAwaitTimeMS;
576584
}
585+
// we check for undefined specifically here to allow falsy values
586+
// eslint-disable-next-line no-restricted-syntax
587+
if (options.comment !== undefined) {
588+
getMoreCmd.comment = options.comment;
589+
}
577590

578591
const commandOptions = Object.assign(
579592
{

src/cursor/abstract_cursor.ts

+78-56
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const kInitialized = Symbol('initialized');
4242
const kClosed = Symbol('closed');
4343
/** @internal */
4444
const kKilled = Symbol('killed');
45+
/** @internal */
46+
const kInit = Symbol('kInit');
4547

4648
/** @public */
4749
export const CURSOR_FLAGS = [
@@ -77,7 +79,15 @@ export interface AbstractCursorOptions extends BSONSerializeOptions {
7779
readConcern?: ReadConcernLike;
7880
batchSize?: number;
7981
maxTimeMS?: number;
80-
comment?: Document | string;
82+
/**
83+
* Comment to apply to the operation.
84+
*
85+
* In server versions pre-4.4, 'comment' must be string. A server
86+
* error will be thrown if any other type is provided.
87+
*
88+
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
89+
*/
90+
comment?: unknown;
8191
tailable?: boolean;
8292
awaitData?: boolean;
8393
noCursorTimeout?: boolean;
@@ -162,7 +172,9 @@ export abstract class AbstractCursor<
162172
this[kOptions].batchSize = options.batchSize;
163173
}
164174

165-
if (options.comment != null) {
175+
// we check for undefined specifically here to allow falsy values
176+
// eslint-disable-next-line no-restricted-syntax
177+
if (options.comment !== undefined) {
166178
this[kOptions].comment = options.comment;
167179
}
168180

@@ -620,6 +632,65 @@ export abstract class AbstractCursor<
620632

621633
executeOperation(this, getMoreOperation, callback);
622634
}
635+
636+
/**
637+
* @internal
638+
*
639+
* This function is exposed for the unified test runner's createChangeStream
640+
* operation. We cannot refactor to use the abstract _initialize method without
641+
* a significant refactor.
642+
*/
643+
[kInit](callback: Callback<TSchema | null>): void {
644+
if (this[kSession] == null) {
645+
if (this[kTopology].shouldCheckForSessionSupport()) {
646+
return this[kTopology].selectServer(ReadPreference.primaryPreferred, {}, err => {
647+
if (err) return callback(err);
648+
return this[kInit](callback);
649+
});
650+
} else if (this[kTopology].hasSessionSupport()) {
651+
this[kSession] = this[kTopology].startSession({ owner: this, explicit: false });
652+
}
653+
}
654+
655+
this._initialize(this[kSession], (err, state) => {
656+
if (state) {
657+
const response = state.response;
658+
this[kServer] = state.server;
659+
this[kSession] = state.session;
660+
661+
if (response.cursor) {
662+
this[kId] =
663+
typeof response.cursor.id === 'number'
664+
? Long.fromNumber(response.cursor.id)
665+
: response.cursor.id;
666+
667+
if (response.cursor.ns) {
668+
this[kNamespace] = ns(response.cursor.ns);
669+
}
670+
671+
this[kDocuments] = response.cursor.firstBatch;
672+
}
673+
674+
// When server responses return without a cursor document, we close this cursor
675+
// and return the raw server response. This is often the case for explain commands
676+
// for example
677+
if (this[kId] == null) {
678+
this[kId] = Long.ZERO;
679+
// TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
680+
this[kDocuments] = [state.response as TODO_NODE_3286];
681+
}
682+
}
683+
684+
// the cursor is now initialized, even if an error occurred or it is dead
685+
this[kInitialized] = true;
686+
687+
if (err || cursorIsDead(this)) {
688+
return cleanupCursor(this, { error: err }, () => callback(err, nextDocument(this)));
689+
}
690+
691+
callback();
692+
});
693+
}
623694
}
624695

625696
function nextDocument<T>(cursor: AbstractCursor): T | null | undefined {
@@ -653,61 +724,12 @@ function next<T>(cursor: AbstractCursor, blocking: boolean, callback: Callback<T
653724

654725
if (cursorId == null) {
655726
// All cursors must operate within a session, one must be made implicitly if not explicitly provided
656-
if (cursor[kSession] == null) {
657-
if (cursor[kTopology].shouldCheckForSessionSupport()) {
658-
return cursor[kTopology].selectServer(ReadPreference.primaryPreferred, {}, err => {
659-
if (err) return callback(err);
660-
return next(cursor, blocking, callback);
661-
});
662-
} else if (cursor[kTopology].hasSessionSupport()) {
663-
cursor[kSession] = cursor[kTopology].startSession({ owner: cursor, explicit: false });
664-
}
665-
}
666-
667-
cursor._initialize(cursor[kSession], (err, state) => {
668-
if (state) {
669-
const response = state.response;
670-
cursor[kServer] = state.server;
671-
cursor[kSession] = state.session;
672-
673-
if (response.cursor) {
674-
cursor[kId] =
675-
typeof response.cursor.id === 'number'
676-
? Long.fromNumber(response.cursor.id)
677-
: response.cursor.id;
678-
679-
if (response.cursor.ns) {
680-
cursor[kNamespace] = ns(response.cursor.ns);
681-
}
682-
683-
cursor[kDocuments] = response.cursor.firstBatch;
684-
} else {
685-
// NOTE: This is for support of older servers (<3.2) which do not use commands
686-
cursor[kId] =
687-
typeof response.cursorId === 'number'
688-
? Long.fromNumber(response.cursorId)
689-
: response.cursorId;
690-
cursor[kDocuments] = response.documents;
691-
}
692-
693-
// When server responses return without a cursor document, we close this cursor
694-
// and return the raw server response. This is often the case for explain commands
695-
// for example
696-
if (cursor[kId] == null) {
697-
cursor[kId] = Long.ZERO;
698-
// TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
699-
cursor[kDocuments] = [state.response as TODO_NODE_3286];
700-
}
701-
}
702-
703-
// the cursor is now initialized, even if an error occurred or it is dead
704-
cursor[kInitialized] = true;
705-
706-
if (err || cursorIsDead(cursor)) {
707-
return cleanupCursor(cursor, { error: err }, () => callback(err, nextDocument(cursor)));
727+
cursor[kInit]((err, value) => {
728+
if (err) return callback(err);
729+
if (value) {
730+
return callback(undefined, value);
708731
}
709-
710-
next(cursor, blocking, callback);
732+
return next(cursor, blocking, callback);
711733
});
712734

713735
return;

src/operations/aggregate.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import type { Document } from '../bson';
22
import { MongoInvalidArgumentError } from '../error';
33
import type { Server } from '../sdam/server';
44
import type { ClientSession } from '../sessions';
5-
import type { Callback } from '../utils';
6-
import { maxWireVersion, MongoDBNamespace } from '../utils';
5+
import { Callback, maxWireVersion, MongoDBNamespace } from '../utils';
76
import { CollationOptions, CommandOperation, CommandOperationOptions } from './command';
87
import { Aspect, defineAspects, Hint } from './operation';
98

@@ -31,6 +30,7 @@ export interface AggregateOptions extends CommandOperationOptions {
3130
hint?: Hint;
3231
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
3332
let?: Document;
33+
3434
out?: string;
3535
}
3636

@@ -121,6 +121,12 @@ export class AggregateOperation<T = Document> extends CommandOperation<T> {
121121
command.let = options.let;
122122
}
123123

124+
// we check for undefined specifically here to allow falsy values
125+
// eslint-disable-next-line no-restricted-syntax
126+
if (options.comment !== undefined) {
127+
command.comment = options.comment;
128+
}
129+
124130
command.cursor = options.cursor || {};
125131
if (options.batchSize && !this.hasWriteStage) {
126132
command.cursor.batchSize = options.batchSize;

src/operations/command.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ export interface CommandOperationOptions
4545
/** Collation */
4646
collation?: CollationOptions;
4747
maxTimeMS?: number;
48-
/** A user-provided comment to attach to this command */
49-
comment?: string | Document;
48+
/**
49+
* Comment to apply to the operation.
50+
*
51+
* In server versions pre-4.4, 'comment' must be string. A server
52+
* error will be thrown if any other type is provided.
53+
*
54+
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
55+
*/
56+
comment?: unknown;
5057
/** Should retry failed writes */
5158
retryWrites?: boolean;
5259

src/operations/delete.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import { Aspect, defineAspects, Hint } from './operation';
1212
export interface DeleteOptions extends CommandOperationOptions, WriteConcernOptions {
1313
/** If true, when an insert fails, don't execute the remaining writes. If false, continue with remaining inserts when one fails. */
1414
ordered?: boolean;
15-
/** A user-provided comment to attach to this command */
16-
comment?: string | Document;
1715
/** Specifies the collation to use for the operation */
1816
collation?: CollationOptions;
1917
/** Specify that the update query should only consider plans using the hinted index */
@@ -43,8 +41,6 @@ export interface DeleteStatement {
4341
collation?: CollationOptions;
4442
/** A document or string that specifies the index to use to support the query predicate. */
4543
hint?: Hint;
46-
/** A user-provided comment to attach to this command */
47-
comment?: string | Document;
4844
}
4945

5046
/** @internal */
@@ -80,6 +76,12 @@ export class DeleteOperation extends CommandOperation<Document> {
8076
command.let = options.let;
8177
}
8278

79+
// we check for undefined specifically here to allow falsy values
80+
// eslint-disable-next-line no-restricted-syntax
81+
if (options.comment !== undefined) {
82+
command.comment = options.comment;
83+
}
84+
8385
if (options.explain != null && maxWireVersion(server) < 3) {
8486
return callback
8587
? callback(
@@ -175,10 +177,6 @@ export function makeDeleteStatement(
175177
op.hint = options.hint;
176178
}
177179

178-
if (options.comment) {
179-
op.comment = options.comment;
180-
}
181-
182180
return op;
183181
}
184182

src/operations/find.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ export interface FindOptions<TSchema extends Document = Document> extends Comman
4646
min?: Document;
4747
/** The exclusive upper bound for a specific index */
4848
max?: Document;
49-
/** You can put a $comment field on a query to make looking in the profiler logs simpler. */
50-
comment?: string | Document;
5149
/** Number of milliseconds to wait before aborting the query. */
5250
maxTimeMS?: number;
5351
/** The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. Requires `tailable` and `awaitData` to be true */
@@ -241,7 +239,9 @@ function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOp
241239
findCommand.singleBatch = options.singleBatch;
242240
}
243241

244-
if (options.comment) {
242+
// we check for undefined specifically here to allow falsy values
243+
// eslint-disable-next-line no-restricted-syntax
244+
if (options.comment !== undefined) {
245245
findCommand.comment = options.comment;
246246
}
247247

src/operations/find_and_modify.ts

+15
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ interface FindAndModifyCmdBase {
8282
maxTimeMS?: number;
8383
let?: Document;
8484
writeConcern?: WriteConcern | WriteConcernSettings;
85+
/**
86+
* Comment to apply to the operation.
87+
*
88+
* In server versions pre-4.4, 'comment' must be string. A server
89+
* error will be thrown if any other type is provided.
90+
*
91+
* In server versions 4.4 and above, 'comment' can be any valid BSON type.
92+
*/
93+
comment?: unknown;
8594
}
8695

8796
function configureFindAndModifyCmdBaseUpdateOpts(
@@ -140,6 +149,12 @@ class FindAndModifyOperation extends CommandOperation<Document> {
140149
this.cmdBase.let = options.let;
141150
}
142151

152+
// we check for undefined specifically here to allow falsy values
153+
// eslint-disable-next-line no-restricted-syntax
154+
if (options.comment !== undefined) {
155+
this.cmdBase.comment = options.comment;
156+
}
157+
143158
// force primary read preference
144159
this.readPreference = ReadPreference.primary;
145160

0 commit comments

Comments
 (0)