Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions lib/helpers/model/castBulkWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = function castBulkWrite(originalModel, op, options) {
const model = decideModelByObject(originalModel, op['insertOne']['document']);

const doc = new model(op['insertOne']['document']);
if (model.schema.options.timestamps && options.timestamps !== false) {
if (model.schema.options.timestamps && getTimestampsOpt(op['insertOne'], options)) {
doc.initializeTimestamps();
}
if (options.session != null) {
Expand Down Expand Up @@ -190,7 +190,7 @@ module.exports = function castBulkWrite(originalModel, op, options) {

// set `skipId`, otherwise we get "_id field cannot be changed"
const doc = new model(op['replaceOne']['replacement'], strict, true);
if (model.schema.options.timestamps) {
if (model.schema.options.timestamps && getTimestampsOpt(op['replaceOne'], options)) {
doc.initializeTimestamps();
}
if (options.session != null) {
Expand Down Expand Up @@ -279,3 +279,21 @@ function decideModelByObject(model, object) {
}
return model;
}

/**
* Gets timestamps option for a given operation. If the option is set within an individual
* operation, use it. Otherwise, use the global timestamps option configured in the `bulkWrite`
* options. Overall default is `true`.
* @api private
*/

function getTimestampsOpt(opCommand, options) {
const opLevelOpt = opCommand.timestamps;
const bulkLevelOpt = options.timestamps;
if (opLevelOpt != null) {
return opLevelOpt;
} else if (bulkLevelOpt != null) {
return bulkLevelOpt;
}
return true;
}
46 changes: 46 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5926,6 +5926,52 @@ describe('Model', function() {

});

it('bulkWrite can disable timestamps with insertOne and replaceOne (gh-15782)', async function() {
const userSchema = new Schema({
name: String
}, { timestamps: true });

const User = db.model('User', userSchema);

const user = await User.create({ name: 'Hafez' });

await User.bulkWrite([
{ insertOne: { document: { name: 'insertOne-test' }, timestamps: false } },
{ replaceOne: { filter: { _id: user._id }, replacement: { name: 'replaceOne-test' }, timestamps: false } }
]);

const insertedDoc = await User.findOne({ name: 'insertOne-test' });
assert.strictEqual(insertedDoc.createdAt, undefined);
assert.strictEqual(insertedDoc.updatedAt, undefined);

const replacedDoc = await User.findOne({ name: 'replaceOne-test' });
assert.strictEqual(replacedDoc.createdAt, undefined);
assert.strictEqual(replacedDoc.updatedAt, undefined);
});

it('bulkWrite insertOne and replaceOne respect per-op timestamps: true when global is false (gh-15782)', async function() {
const userSchema = new Schema({
name: String
}, { timestamps: true });

const User = db.model('User', userSchema);

const user = await User.create({ name: 'Hafez' });

await User.bulkWrite([
{ insertOne: { document: { name: 'insertOne-test' }, timestamps: true } },
{ replaceOne: { filter: { _id: user._id }, replacement: { name: 'replaceOne-test' }, timestamps: true } }
], { timestamps: false });

const insertedDoc = await User.findOne({ name: 'insertOne-test' });
assert.ok(insertedDoc.createdAt instanceof Date);
assert.ok(insertedDoc.updatedAt instanceof Date);

const replacedDoc = await User.findOne({ name: 'replaceOne-test' });
assert.ok(replacedDoc.createdAt instanceof Date);
assert.ok(replacedDoc.updatedAt instanceof Date);
});

it('bulkwrite should not change updatedAt on subdocs when timestamps set to false (gh-13611)', async function() {

const postSchema = new Schema({
Expand Down
41 changes: 38 additions & 3 deletions test/types/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import mongoose, {
Query,
UpdateWriteOpResult,
AggregateOptions,
StringSchemaDefinition
StringSchemaDefinition,
UpdateOneModel,
UpdateManyModel
} from 'mongoose';
import { expectAssignable, expectError, expectType } from 'tsd';
import { AutoTypedSchemaType, autoTypedSchema } from './schema.test';
import { UpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb';
import { UpdateOneModel as MongoUpdateOneModel, ChangeStreamInsertDocument, ObjectId } from 'mongodb';

function rawDocSyntax(): void {
interface ITest {
Expand Down Expand Up @@ -415,7 +417,7 @@ function gh11911() {
const Animal = model<IAnimal>('Animal', animalSchema);

const changes: UpdateQuery<IAnimal> = {};
expectAssignable<UpdateOneModel>({
expectAssignable<MongoUpdateOneModel>({
filter: {},
update: changes
});
Expand Down Expand Up @@ -766,3 +768,36 @@ async function gh14003() {
await TestModel.validate({ name: 'foo' }, ['name']);
await TestModel.validate({ name: 'foo' }, { pathsToSkip: ['name'] });
}

async function gh15781() {
const userSchema = new Schema({
createdAt: { type: Date, immutable: true },
name: String
}, { timestamps: true });

const User = model('User', userSchema);

await User.bulkWrite([
{
updateOne: {
filter: { name: 'John' },
update: { createdAt: new Date() },
overwriteImmutable: true,
timestamps: false
}
},
{
updateMany: {
filter: { name: 'Jane' },
update: { createdAt: new Date() },
overwriteImmutable: true,
timestamps: false
}
}
]);

expectType<boolean | undefined>({} as UpdateOneModel['timestamps']);
expectType<boolean | undefined>({} as UpdateOneModel['overwriteImmutable']);
expectType<boolean | undefined>({} as UpdateManyModel['timestamps']);
expectType<boolean | undefined>({} as UpdateManyModel['overwriteImmutable']);
}
103 changes: 101 additions & 2 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,105 @@ declare module 'mongoose' {

interface RemoveOptions extends SessionOption, Omit<mongodb.DeleteOptions, 'session'> {}

export type AnyBulkWriteOperation<TSchema = AnyObject> = {
insertOne: InsertOneModel<TSchema>;
} | {
replaceOne: ReplaceOneModel<TSchema>;
} | {
updateOne: UpdateOneModel<TSchema>;
} | {
updateMany: UpdateManyModel<TSchema>;
} | {
deleteOne: DeleteOneModel<TSchema>;
} | {
deleteMany: DeleteManyModel<TSchema>;
};

export interface InsertOneModel<TSchema> {
document: mongodb.OptionalId<TSchema>;
/** Skip validation for this operation. */
skipValidation?: boolean;
/** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */
timestamps?: boolean;
}

export interface ReplaceOneModel<TSchema = AnyObject> {
/** The filter to limit the replaced document. */
filter: FilterQuery<TSchema>;
/** The document with which to replace the matched document. */
replacement: mongodb.WithoutId<TSchema>;
/** Specifies a collation. */
collation?: mongodb.CollationOptions;
/** The index to use. If specified, then the query system will only consider plans using the hinted index. */
hint?: mongodb.Hint;
/** When true, creates a new document if no document matches the query. */
upsert?: boolean;
/** Skip validation for this operation. */
skipValidation?: boolean;
/** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */
timestamps?: boolean;
}

export interface UpdateOneModel<TSchema = AnyObject> {
/** The filter to limit the updated documents. */
filter: FilterQuery<TSchema>;
/** A document or pipeline containing update operators. */
update: UpdateQuery<TSchema> | UpdateQuery<TSchema>[];
/** A set of filters specifying to which array elements an update should apply. */
arrayFilters?: AnyObject[];
/** Specifies a collation. */
collation?: mongodb.CollationOptions;
/** The index to use. If specified, then the query system will only consider plans using the hinted index. */
hint?: mongodb.Hint;
/** When true, creates a new document if no document matches the query. */
upsert?: boolean;
/** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */
timestamps?: boolean;
/** When true, allows updating fields that are marked as `immutable` in the schema. */
overwriteImmutable?: boolean;
/** When false, do not set default values on insert. */
setDefaultsOnInsert?: boolean;
}

export interface UpdateManyModel<TSchema = AnyObject> {
/** The filter to limit the updated documents. */
filter: FilterQuery<TSchema>;
/** A document or pipeline containing update operators. */
update: UpdateQuery<TSchema> | UpdateQuery<TSchema>[];
/** A set of filters specifying to which array elements an update should apply. */
arrayFilters?: AnyObject[];
/** Specifies a collation. */
collation?: mongodb.CollationOptions;
/** The index to use. If specified, then the query system will only consider plans using the hinted index. */
hint?: mongodb.Hint;
/** When true, creates a new document if no document matches the query. */
upsert?: boolean;
/** When false, do not add timestamps. When true, overrides the `timestamps` option set in the `bulkWrite` options. */
timestamps?: boolean;
/** When true, allows updating fields that are marked as `immutable` in the schema. */
overwriteImmutable?: boolean;
/** When false, do not set default values on insert. */
setDefaultsOnInsert?: boolean;
}

export interface DeleteOneModel<TSchema = AnyObject> {
/** The filter to limit the deleted documents. */
filter: FilterQuery<TSchema>;
/** Specifies a collation. */
collation?: mongodb.CollationOptions;
/** The index to use. If specified, then the query system will only consider plans using the hinted index. */
hint?: mongodb.Hint;
}

export interface DeleteManyModel<TSchema = AnyObject> {
/** The filter to limit the deleted documents. */
filter: FilterQuery<TSchema>;
/** Specifies a collation. */
collation?: mongodb.CollationOptions;
/** The index to use. If specified, then the query system will only consider plans using the hinted index. */
hint?: mongodb.Hint;
}

const Model: Model<any>;

/**
Expand Down Expand Up @@ -185,11 +284,11 @@ declare module 'mongoose' {
* round trip to the MongoDB server.
*/
bulkWrite<DocContents = TRawDocType>(
writes: Array<mongodb.AnyBulkWriteOperation<DocContents extends Document ? any : (DocContents extends {} ? DocContents : any)>>,
writes: Array<AnyBulkWriteOperation<DocContents extends Document ? any : (DocContents extends {} ? DocContents : any)>>,
options: mongodb.BulkWriteOptions & MongooseBulkWriteOptions & { ordered: false }
): Promise<mongodb.BulkWriteResult & { mongoose?: { validationErrors: Error[] } }>;
bulkWrite<DocContents = TRawDocType>(
writes: Array<mongodb.AnyBulkWriteOperation<DocContents extends Document ? any : (DocContents extends {} ? DocContents : any)>>,
writes: Array<AnyBulkWriteOperation<DocContents extends Document ? any : (DocContents extends {} ? DocContents : any)>>,
options?: mongodb.BulkWriteOptions & MongooseBulkWriteOptions
): Promise<mongodb.BulkWriteResult>;

Expand Down
Loading