Skip to content

Commit 0d91da1

Browse files
authored
feat(NODE-3333): support 'let' option for CRUD commands (#2829)
1 parent 5a6279a commit 0d91da1

File tree

9 files changed

+910
-5
lines changed

9 files changed

+910
-5
lines changed

src/operations/delete.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface DeleteOptions extends CommandOperationOptions, WriteConcernOpti
1818
collation?: CollationOptions;
1919
/** Specify that the update query should only consider plans using the hinted index */
2020
hint?: string | Document;
21+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
22+
let?: Document;
2123

2224
/** @deprecated use `removeOne` or `removeMany` to implicitly specify the limit */
2325
single?: boolean;
@@ -74,6 +76,10 @@ export class DeleteOperation extends CommandOperation<Document> {
7476
ordered
7577
};
7678

79+
if (options.let) {
80+
command.let = options.let;
81+
}
82+
7783
if (options.explain !== undefined && maxWireVersion(server) < 3) {
7884
return callback
7985
? callback(new MongoError(`server ${server.name} does not support explain on delete`))

src/operations/find.ts

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface FindOptions<TSchema = Document> extends CommandOperationOptions
6161
allowPartialResults?: boolean;
6262
/** Determines whether to return the record identifier for each document. If true, adds a field $recordId to the returned documents. */
6363
showRecordId?: boolean;
64+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
65+
let?: Document;
6466
}
6567

6668
const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5;
@@ -286,6 +288,10 @@ function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOp
286288
findCommand.allowDiskUse = options.allowDiskUse;
287289
}
288290

291+
if (options.let) {
292+
findCommand.let = options.let;
293+
}
294+
289295
return findCommand;
290296
}
291297

src/operations/find_and_modify.ts

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface FindOneAndDeleteOptions extends CommandOperationOptions {
2727
projection?: Document;
2828
/** Determines which document the operation modifies if the query selects multiple documents. */
2929
sort?: Sort;
30+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
31+
let?: Document;
3032
}
3133

3234
/** @public */
@@ -43,6 +45,8 @@ export interface FindOneAndReplaceOptions extends CommandOperationOptions {
4345
sort?: Sort;
4446
/** Upsert the document if it does not exist. */
4547
upsert?: boolean;
48+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
49+
let?: Document;
4650
}
4751

4852
/** @public */
@@ -61,6 +65,8 @@ export interface FindOneAndUpdateOptions extends CommandOperationOptions {
6165
sort?: Sort;
6266
/** Upsert the document if it does not exist. */
6367
upsert?: boolean;
68+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
69+
let?: Document;
6470
}
6571

6672
/** @internal */
@@ -74,6 +80,7 @@ interface FindAndModifyCmdBase {
7480
bypassDocumentValidation?: boolean;
7581
arrayFilters?: Document[];
7682
maxTimeMS?: number;
83+
let?: Document;
7784
writeConcern?: WriteConcern | WriteConcernSettings;
7885
}
7986

@@ -129,6 +136,10 @@ class FindAndModifyOperation extends CommandOperation<Document> {
129136
this.cmdBase.writeConcern = options.writeConcern;
130137
}
131138

139+
if (options.let) {
140+
this.cmdBase.let = options.let;
141+
}
142+
132143
// force primary read preference
133144
this.readPreference = ReadPreference.primary;
134145

src/operations/update.ts

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface UpdateOptions extends CommandOperationOptions {
2525
hint?: string | Document;
2626
/** When true, creates a new document if no document matches the query */
2727
upsert?: boolean;
28+
/** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */
29+
let?: Document;
2830
}
2931

3032
/** @public */
@@ -97,6 +99,10 @@ export class UpdateOperation extends CommandOperation<Document> {
9799
command.bypassDocumentValidation = options.bypassDocumentValidation;
98100
}
99101

102+
if (options.let) {
103+
command.let = options.let;
104+
}
105+
100106
const statementWithCollation = this.statements.find(statement => !!statement.collation);
101107
if (
102108
collationNotSupported(server, options) ||

test/functional/unified-spec-runner/operations.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ operations.set('createIndex', async ({ entities, operation }) => {
215215

216216
operations.set('deleteOne', async ({ entities, operation }) => {
217217
const collection = entities.getEntity('collection', operation.object);
218-
return collection.deleteOne(operation.arguments.filter);
218+
const { filter, ...options } = operation.arguments;
219+
return collection.deleteOne(filter, options);
219220
});
220221

221222
operations.set('dropCollection', async ({ entities, operation }) => {
@@ -230,8 +231,8 @@ operations.set('endSession', async ({ entities, operation }) => {
230231

231232
operations.set('find', async ({ entities, operation }) => {
232233
const collection = entities.getEntity('collection', operation.object);
233-
const { filter, sort, batchSize, limit } = operation.arguments;
234-
return collection.find(filter, { sort, batchSize, limit }).toArray();
234+
const { filter, sort, batchSize, limit, let: vars } = operation.arguments;
235+
return collection.find(filter, { sort, batchSize, limit, let: vars }).toArray();
235236
});
236237

237238
operations.set('findOneAndReplace', async ({ entities, operation }) => {
@@ -398,12 +399,14 @@ operations.set('runCommand', async ({ entities, operation }: OperationFunctionPa
398399

399400
operations.set('updateMany', async ({ entities, operation }) => {
400401
const collection = entities.getEntity('collection', operation.object);
401-
return collection.updateMany(operation.arguments.filter, operation.arguments.update);
402+
const { filter, update, ...options } = operation.arguments;
403+
return collection.updateMany(filter, update, options);
402404
});
403405

404406
operations.set('updateOne', async ({ entities, operation }) => {
405407
const collection = entities.getEntity('collection', operation.object);
406-
return collection.updateOne(operation.arguments.filter, operation.arguments.update);
408+
const { filter, update, ...options } = operation.arguments;
409+
return collection.updateOne(filter, update, options);
407410
});
408411

409412
export async function executeOperationAndCheck(

test/spec/crud/unified/aggregate-let.json

+103
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,109 @@
4444
"minServerVersion": "5.0"
4545
}
4646
],
47+
"operations": [
48+
{
49+
"name": "aggregate",
50+
"object": "collection0",
51+
"arguments": {
52+
"pipeline": [
53+
{
54+
"$match": {
55+
"$expr": {
56+
"$eq": [
57+
"$_id",
58+
"$$id"
59+
]
60+
}
61+
}
62+
},
63+
{
64+
"$project": {
65+
"_id": 0,
66+
"x": "$$x",
67+
"y": "$$y",
68+
"rand": "$$rand"
69+
}
70+
}
71+
],
72+
"let": {
73+
"id": 1,
74+
"x": "foo",
75+
"y": {
76+
"$literal": "bar"
77+
},
78+
"rand": {
79+
"$rand": {}
80+
}
81+
}
82+
},
83+
"expectResult": [
84+
{
85+
"x": "foo",
86+
"y": "bar",
87+
"rand": {
88+
"$$type": "double"
89+
}
90+
}
91+
]
92+
}
93+
],
94+
"expectEvents": [
95+
{
96+
"client": "client0",
97+
"events": [
98+
{
99+
"commandStartedEvent": {
100+
"command": {
101+
"aggregate": "coll0",
102+
"pipeline": [
103+
{
104+
"$match": {
105+
"$expr": {
106+
"$eq": [
107+
"$_id",
108+
"$$id"
109+
]
110+
}
111+
}
112+
},
113+
{
114+
"$project": {
115+
"_id": 0,
116+
"x": "$$x",
117+
"y": "$$y",
118+
"rand": "$$rand"
119+
}
120+
}
121+
],
122+
"let": {
123+
"id": 1,
124+
"x": "foo",
125+
"y": {
126+
"$literal": "bar"
127+
},
128+
"rand": {
129+
"$rand": {}
130+
}
131+
}
132+
}
133+
}
134+
}
135+
]
136+
}
137+
]
138+
},
139+
{
140+
"description": "Aggregate with let option and dollar-prefixed $literal value",
141+
"runOnRequirements": [
142+
{
143+
"minServerVersion": "5.0",
144+
"topologies": [
145+
"single",
146+
"replicaset"
147+
]
148+
}
149+
],
47150
"operations": [
48151
{
49152
"name": "aggregate",

test/spec/crud/unified/aggregate-let.yml

+36
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,45 @@ initialData: &initialData
2222
- { _id: 1 }
2323

2424
tests:
25+
# TODO: Once SERVER-57403 is resolved, this test can be removed in favor of
26+
# the "dollar-prefixed $literal value" test below.
2527
- description: "Aggregate with let option"
2628
runOnRequirements:
2729
- minServerVersion: "5.0"
30+
operations:
31+
- name: aggregate
32+
object: *collection0
33+
arguments:
34+
pipeline: &pipeline0
35+
# $match takes a query expression, so $expr is necessary to utilize
36+
# an aggregate expression context and access "let" variables.
37+
- $match: { $expr: { $eq: ["$_id", "$$id"] } }
38+
- $project: { _id: 0, x: "$$x", y: "$$y", rand: "$$rand" }
39+
# Values in "let" must be constant or closed expressions that do not
40+
# depend on document values. This test demonstrates a basic constant
41+
# value, a value wrapped with $literal (to avoid expression parsing),
42+
# and a closed expression (e.g. $rand).
43+
let: &let0
44+
id: 1
45+
x: foo
46+
y: { $literal: "bar" }
47+
rand: { $rand: {} }
48+
expectResult:
49+
- { x: "foo", y: "bar", rand: { $$type: "double" } }
50+
expectEvents:
51+
- client: *client0
52+
events:
53+
- commandStartedEvent:
54+
command:
55+
aggregate: *collection0Name
56+
pipeline: *pipeline0
57+
let: *let0
58+
59+
- description: "Aggregate with let option and dollar-prefixed $literal value"
60+
runOnRequirements:
61+
- minServerVersion: "5.0"
62+
# TODO: Remove topology restrictions once SERVER-57403 is resolved
63+
topologies: ["single", "replicaset"]
2864
operations:
2965
- name: aggregate
3066
object: *collection0

0 commit comments

Comments
 (0)