Skip to content

Commit fbb5059

Browse files
fix(NODE-5636): generate _ids using pkFactory in bulk write operations (#4025)
1 parent 2348548 commit fbb5059

File tree

7 files changed

+142
-92
lines changed

7 files changed

+142
-92
lines changed

src/bulk/common.ts

+21-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { promisify } from 'util';
22

3-
import { type BSONSerializeOptions, type Document, ObjectId, resolveBSONOptions } from '../bson';
3+
import { type BSONSerializeOptions, type Document, resolveBSONOptions } from '../bson';
44
import type { Collection } from '../collection';
55
import {
66
type AnyError,
@@ -12,6 +12,7 @@ import {
1212
} from '../error';
1313
import type { Filter, OneOrMore, OptionalId, UpdateFilter, WithoutId } from '../mongo_types';
1414
import type { CollationOptions, CommandOperationOptions } from '../operations/command';
15+
import { maybeAddIdToDocuments } from '../operations/common_functions';
1516
import { DeleteOperation, type DeleteStatement, makeDeleteStatement } from '../operations/delete';
1617
import { executeOperation } from '../operations/execute_operation';
1718
import { InsertOperation } from '../operations/insert';
@@ -917,7 +918,7 @@ export abstract class BulkOperationBase {
917918
* Create a new OrderedBulkOperation or UnorderedBulkOperation instance
918919
* @internal
919920
*/
920-
constructor(collection: Collection, options: BulkWriteOptions, isOrdered: boolean) {
921+
constructor(private collection: Collection, options: BulkWriteOptions, isOrdered: boolean) {
921922
// determine whether bulkOperation is ordered or unordered
922923
this.isOrdered = isOrdered;
923924

@@ -1032,9 +1033,9 @@ export abstract class BulkOperationBase {
10321033
* ```
10331034
*/
10341035
insert(document: Document): BulkOperationBase {
1035-
if (document._id == null && !shouldForceServerObjectId(this)) {
1036-
document._id = new ObjectId();
1037-
}
1036+
maybeAddIdToDocuments(this.collection, document, {
1037+
forceServerObjectId: this.shouldForceServerObjectId()
1038+
});
10381039

10391040
return this.addToOperationsList(BatchType.INSERT, document);
10401041
}
@@ -1093,21 +1094,16 @@ export abstract class BulkOperationBase {
10931094
throw new MongoInvalidArgumentError('Operation must be an object with an operation key');
10941095
}
10951096
if ('insertOne' in op) {
1096-
const forceServerObjectId = shouldForceServerObjectId(this);
1097-
if (op.insertOne && op.insertOne.document == null) {
1098-
// NOTE: provided for legacy support, but this is a malformed operation
1099-
if (forceServerObjectId !== true && (op.insertOne as Document)._id == null) {
1100-
(op.insertOne as Document)._id = new ObjectId();
1101-
}
1102-
1103-
return this.addToOperationsList(BatchType.INSERT, op.insertOne);
1104-
}
1097+
const forceServerObjectId = this.shouldForceServerObjectId();
1098+
const document =
1099+
op.insertOne && op.insertOne.document == null
1100+
? // TODO(NODE-6003): remove support for omitting the `documents` subdocument in bulk inserts
1101+
(op.insertOne as Document)
1102+
: op.insertOne.document;
11051103

1106-
if (forceServerObjectId !== true && op.insertOne.document._id == null) {
1107-
op.insertOne.document._id = new ObjectId();
1108-
}
1104+
maybeAddIdToDocuments(this.collection, document, { forceServerObjectId });
11091105

1110-
return this.addToOperationsList(BatchType.INSERT, op.insertOne.document);
1106+
return this.addToOperationsList(BatchType.INSERT, document);
11111107
}
11121108

11131109
if ('replaceOne' in op || 'updateOne' in op || 'updateMany' in op) {
@@ -1268,6 +1264,13 @@ export abstract class BulkOperationBase {
12681264
batchType: BatchType,
12691265
document: Document | UpdateStatement | DeleteStatement
12701266
): this;
1267+
1268+
private shouldForceServerObjectId(): boolean {
1269+
return (
1270+
this.s.options.forceServerObjectId === true ||
1271+
this.s.collection.s.db.options?.forceServerObjectId === true
1272+
);
1273+
}
12711274
}
12721275

12731276
Object.defineProperty(BulkOperationBase.prototype, 'length', {
@@ -1277,18 +1280,6 @@ Object.defineProperty(BulkOperationBase.prototype, 'length', {
12771280
}
12781281
});
12791282

1280-
function shouldForceServerObjectId(bulkOperation: BulkOperationBase): boolean {
1281-
if (typeof bulkOperation.s.options.forceServerObjectId === 'boolean') {
1282-
return bulkOperation.s.options.forceServerObjectId;
1283-
}
1284-
1285-
if (typeof bulkOperation.s.collection.s.db.options?.forceServerObjectId === 'boolean') {
1286-
return bulkOperation.s.collection.s.db.options?.forceServerObjectId;
1287-
}
1288-
1289-
return false;
1290-
}
1291-
12921283
function isInsertBatch(batch: Batch): boolean {
12931284
return batch.batchType === BatchType.INSERT;
12941285
}

src/mongo_client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface Auth {
8686

8787
/** @public */
8888
export interface PkFactory {
89-
createPk(): any; // TODO: when js-bson is typed, function should return some BSON type
89+
createPk(): any;
9090
}
9191

9292
/** @public */

src/operations/common_functions.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,37 @@ export async function indexInformation(
4343
return info;
4444
}
4545

46-
export function prepareDocs(
46+
export function maybeAddIdToDocuments(
4747
coll: Collection,
4848
docs: Document[],
4949
options: { forceServerObjectId?: boolean }
50-
): Document[] {
50+
): Document[];
51+
export function maybeAddIdToDocuments(
52+
coll: Collection,
53+
docs: Document,
54+
options: { forceServerObjectId?: boolean }
55+
): Document;
56+
export function maybeAddIdToDocuments(
57+
coll: Collection,
58+
docOrDocs: Document[] | Document,
59+
options: { forceServerObjectId?: boolean }
60+
): Document[] | Document {
5161
const forceServerObjectId =
5262
typeof options.forceServerObjectId === 'boolean'
5363
? options.forceServerObjectId
5464
: coll.s.db.options?.forceServerObjectId;
5565

5666
// no need to modify the docs if server sets the ObjectId
5767
if (forceServerObjectId === true) {
58-
return docs;
68+
return docOrDocs;
5969
}
6070

61-
return docs.map(doc => {
71+
const transform = (doc: Document): Document => {
6272
if (doc._id == null) {
6373
doc._id = coll.s.pkFactory.createPk();
6474
}
6575

6676
return doc;
67-
});
77+
};
78+
return Array.isArray(docOrDocs) ? docOrDocs.map(transform) : transform(docOrDocs);
6879
}

src/operations/insert.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { MongoDBNamespace } from '../utils';
99
import { WriteConcern } from '../write_concern';
1010
import { BulkWriteOperation } from './bulk_write';
1111
import { CommandOperation, type CommandOperationOptions } from './command';
12-
import { prepareDocs } from './common_functions';
12+
import { maybeAddIdToDocuments } from './common_functions';
1313
import { AbstractOperation, Aspect, defineAspects } from './operation';
1414

1515
/** @internal */
@@ -69,7 +69,7 @@ export interface InsertOneResult<TSchema = Document> {
6969

7070
export class InsertOneOperation extends InsertOperation {
7171
constructor(collection: Collection, doc: Document, options: InsertOneOptions) {
72-
super(collection.s.namespace, prepareDocs(collection, [doc], options), options);
72+
super(collection.s.namespace, maybeAddIdToDocuments(collection, [doc], options), options);
7373
}
7474

7575
override async execute(
@@ -131,7 +131,9 @@ export class InsertManyOperation extends AbstractOperation<InsertManyResult> {
131131
const writeConcern = WriteConcern.fromOptions(options);
132132
const bulkWriteOperation = new BulkWriteOperation(
133133
coll,
134-
prepareDocs(coll, this.docs, options).map(document => ({ insertOne: { document } })),
134+
this.docs.map(document => ({
135+
insertOne: { document }
136+
})),
135137
options
136138
);
137139

test/integration/crud/bulk.test.ts

+71-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as crypto from 'crypto';
33

44
import {
55
type Collection,
6+
Double,
67
Long,
78
MongoBatchReExecutionError,
89
MongoBulkWriteError,
@@ -65,16 +66,85 @@ describe('Bulk', function () {
6566
context('when called with a valid operation', function () {
6667
it('should not throw a MongoInvalidArgument error', async function () {
6768
try {
68-
client.db('test').collection('test').initializeUnorderedBulkOp().raw({ insertOne: {} });
69+
client
70+
.db('test')
71+
.collection('test')
72+
.initializeUnorderedBulkOp()
73+
.raw({ insertOne: { document: {} } });
6974
} catch (error) {
7075
expect(error).not.to.exist;
7176
}
7277
});
7378
});
79+
80+
it('supports the legacy specification (no nested document field)', async function () {
81+
await client
82+
.db('test')
83+
.collection('test')
84+
.initializeUnorderedBulkOp()
85+
// @ts-expect-error Not allowed in TS, but allowed for legacy compat
86+
.raw({ insertOne: { name: 'john doe' } })
87+
.execute();
88+
const result = await client.db('test').collection('test').findOne({ name: 'john doe' });
89+
expect(result).to.exist;
90+
});
7491
});
7592
});
7693

7794
describe('Collection', function () {
95+
describe('when a pkFactory is set on the client', function () {
96+
let client: MongoClient;
97+
const pkFactory = {
98+
count: 0,
99+
createPk: function () {
100+
return new Double(this.count++);
101+
}
102+
};
103+
let collection: Collection;
104+
105+
beforeEach(async function () {
106+
client = this.configuration.newClient({}, { pkFactory, promoteValues: false });
107+
collection = client.db('integration').collection('pk_factory_tests');
108+
await collection.deleteMany({});
109+
});
110+
111+
afterEach(() => client.close());
112+
113+
it('insertMany() generates _ids using the pkFactory', async function () {
114+
await collection.insertMany([{ name: 'john doe' }]);
115+
const result = await collection.findOne({ name: 'john doe' });
116+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
117+
});
118+
119+
it('bulkWrite() generates _ids using the pkFactory', async function () {
120+
await collection.bulkWrite([{ insertOne: { document: { name: 'john doe' } } }]);
121+
const result = await collection.findOne({ name: 'john doe' });
122+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
123+
});
124+
125+
it('ordered bulk operations generate _ids using pkFactory', async function () {
126+
await collection.initializeOrderedBulkOp().insert({ name: 'john doe' }).execute();
127+
const result = await collection.findOne({ name: 'john doe' });
128+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
129+
});
130+
131+
it('unordered bulk operations generate _ids using pkFactory', async function () {
132+
await collection.initializeUnorderedBulkOp().insert({ name: 'john doe' }).execute();
133+
const result = await collection.findOne({ name: 'john doe' });
134+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
135+
});
136+
137+
it('bulkOperation.raw() with the legacy syntax (no nested document field) generates _ids using pkFactory', async function () {
138+
await collection
139+
.initializeOrderedBulkOp()
140+
// @ts-expect-error Not allowed by TS, but still permitted.
141+
.raw({ insertOne: { name: 'john doe' } })
142+
.execute();
143+
const result = await collection.findOne({ name: 'john doe' });
144+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
145+
});
146+
});
147+
78148
describe('#insertMany()', function () {
79149
context('when passed an invalid docs argument', function () {
80150
it('should throw a MongoInvalidArgument error', async function () {

test/integration/crud/insert.test.js

+28-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@ describe('crud - insert', function () {
7171
await client.close();
7272
});
7373

74+
describe('when a pkFactory is set on the client', function () {
75+
let client;
76+
const pkFactory = {
77+
count: 0,
78+
createPk: function () {
79+
return new Double(this.count++);
80+
}
81+
};
82+
let collection;
83+
84+
beforeEach(async function () {
85+
client = this.configuration.newClient({}, { pkFactory, promoteValues: false });
86+
collection = client.db('integration').collection('pk_factory_tests');
87+
await collection.deleteMany({});
88+
});
89+
90+
afterEach(() => client.close());
91+
92+
it('insertOne() generates _ids using the pkFactory', async function () {
93+
await collection.insertOne({ name: 'john doe' });
94+
const result = await collection.findOne({ name: 'john doe' });
95+
expect(result).to.have.property('_id').to.have.property('_bsontype').to.equal('Double');
96+
});
97+
});
98+
7499
it('Should correctly execute Collection.prototype.insertOne', function (done) {
75100
const configuration = this.configuration;
76101
let url = configuration.url();
@@ -135,6 +160,7 @@ describe('crud - insert', function () {
135160
it('insertMany returns the insertedIds and we can look up the documents', async function () {
136161
const db = client.db();
137162
const collection = db.collection('test_multiple_insert');
163+
await collection.deleteMany({});
138164
const docs = [{ a: 1 }, { a: 2 }];
139165

140166
const r = await collection.insertMany(docs);
@@ -839,6 +865,7 @@ describe('crud - insert', function () {
839865

840866
const db = client.db();
841867
const collection = db.collection('Should_correctly_insert_object_with_timestamps');
868+
await collection.deleteMany({});
842869

843870
const { insertedId } = await collection.insertOne(doc);
844871
expect(insertedId.equals(doc._id)).to.be.true;
@@ -1700,7 +1727,7 @@ describe('crud - insert', function () {
17001727
try {
17011728
db.collection(k.toString());
17021729
test.fail(false);
1703-
} catch (err) { } // eslint-disable-line
1730+
} catch (err) {} // eslint-disable-line
17041731

17051732
client.close(done);
17061733
});

test/integration/crud/pk_factory.test.js

-51
This file was deleted.

0 commit comments

Comments
 (0)