diff --git a/src/index.ts b/src/index.ts index 9538ce1d5cc..39d4df719de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -479,6 +479,7 @@ export type { export type { AnyClientBulkWriteModel, ClientBulkWriteError, + ClientBulkWriteModel, ClientBulkWriteOptions, ClientBulkWriteResult, ClientDeleteManyModel, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 49201910362..87b50b42f6a 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -31,7 +31,7 @@ import { } from './mongo_logger'; import { TypedEventEmitter } from './mongo_types'; import { - type AnyClientBulkWriteModel, + type ClientBulkWriteModel, type ClientBulkWriteOptions, type ClientBulkWriteResult } from './operations/client_bulk_write/common'; @@ -331,7 +331,6 @@ export type MongoClientEvents = Pick implements * @param options - The client bulk write options. * @returns A ClientBulkWriteResult for acknowledged writes and ok: 1 for unacknowledged writes. */ - async bulkWrite( - models: AnyClientBulkWriteModel[], + async bulkWrite = Record>( + models: ReadonlyArray>, options?: ClientBulkWriteOptions - ): Promise { + ): Promise { if (this.autoEncrypter) { throw new MongoInvalidArgumentError( 'MongoClient bulkWrite does not currently support automatic encryption.' ); } - return await new ClientBulkWriteExecutor(this, models, options).execute(); + // We do not need schema type information past this point ("as any" is fine) + return await new ClientBulkWriteExecutor( + this, + models as any, + resolveOptions(this, options) + ).execute(); } /** diff --git a/src/operations/client_bulk_write/command_builder.ts b/src/operations/client_bulk_write/command_builder.ts index f85a91b16b5..e4ad79d5e76 100644 --- a/src/operations/client_bulk_write/command_builder.ts +++ b/src/operations/client_bulk_write/command_builder.ts @@ -36,7 +36,7 @@ const MESSAGE_OVERHEAD_BYTES = 1000; /** @internal */ export class ClientBulkWriteCommandBuilder { - models: AnyClientBulkWriteModel[]; + models: ReadonlyArray>; options: ClientBulkWriteOptions; pkFactory: PkFactory; /** The current index in the models array that is being processed. */ @@ -53,7 +53,7 @@ export class ClientBulkWriteCommandBuilder { * @param models - The client write models. */ constructor( - models: AnyClientBulkWriteModel[], + models: ReadonlyArray>, options: ClientBulkWriteOptions, pkFactory?: PkFactory ) { @@ -248,7 +248,7 @@ interface ClientInsertOperation { * @returns the operation. */ export const buildInsertOneOperation = ( - model: ClientInsertOneModel, + model: ClientInsertOneModel, index: number, pkFactory: PkFactory ): ClientInsertOperation => { @@ -275,7 +275,10 @@ export interface ClientDeleteOperation { * @param index - The namespace index. * @returns the operation. */ -export const buildDeleteOneOperation = (model: ClientDeleteOneModel, index: number): Document => { +export const buildDeleteOneOperation = ( + model: ClientDeleteOneModel, + index: number +): Document => { return createDeleteOperation(model, index, false); }; @@ -285,7 +288,10 @@ export const buildDeleteOneOperation = (model: ClientDeleteOneModel, index: numb * @param index - The namespace index. * @returns the operation. */ -export const buildDeleteManyOperation = (model: ClientDeleteManyModel, index: number): Document => { +export const buildDeleteManyOperation = ( + model: ClientDeleteManyModel, + index: number +): Document => { return createDeleteOperation(model, index, true); }; @@ -293,7 +299,7 @@ export const buildDeleteManyOperation = (model: ClientDeleteManyModel, index: nu * Creates a delete operation based on the parameters. */ function createDeleteOperation( - model: ClientDeleteOneModel | ClientDeleteManyModel, + model: ClientDeleteOneModel | ClientDeleteManyModel, index: number, multi: boolean ): ClientDeleteOperation { @@ -330,7 +336,7 @@ export interface ClientUpdateOperation { * @returns the operation. */ export const buildUpdateOneOperation = ( - model: ClientUpdateOneModel, + model: ClientUpdateOneModel, index: number ): ClientUpdateOperation => { return createUpdateOperation(model, index, false); @@ -343,7 +349,7 @@ export const buildUpdateOneOperation = ( * @returns the operation. */ export const buildUpdateManyOperation = ( - model: ClientUpdateManyModel, + model: ClientUpdateManyModel, index: number ): ClientUpdateOperation => { return createUpdateOperation(model, index, true); @@ -365,7 +371,7 @@ function validateUpdate(update: Document) { * Creates a delete operation based on the parameters. */ function createUpdateOperation( - model: ClientUpdateOneModel | ClientUpdateManyModel, + model: ClientUpdateOneModel | ClientUpdateManyModel, index: number, multi: boolean ): ClientUpdateOperation { @@ -413,7 +419,7 @@ export interface ClientReplaceOneOperation { * @returns the operation. */ export const buildReplaceOneOperation = ( - model: ClientReplaceOneModel, + model: ClientReplaceOneModel, index: number ): ClientReplaceOneOperation => { if (hasAtomicOperators(model.replacement)) { @@ -442,7 +448,7 @@ export const buildReplaceOneOperation = ( /** @internal */ export function buildOperation( - model: AnyClientBulkWriteModel, + model: AnyClientBulkWriteModel, index: number, pkFactory: PkFactory ): Document { diff --git a/src/operations/client_bulk_write/common.ts b/src/operations/client_bulk_write/common.ts index 11234cf4eac..c5b96d217ae 100644 --- a/src/operations/client_bulk_write/common.ts +++ b/src/operations/client_bulk_write/common.ts @@ -27,25 +27,32 @@ export interface ClientBulkWriteOptions extends CommandOperationOptions { /** @public */ export interface ClientWriteModel { - /** The namespace for the write. */ + /** + * The namespace for the write. + * + * A namespace is a combination of the database name and the name of the collection: `.`. + * All documents belong to a namespace. + * + * @see https://www.mongodb.com/docs/manual/reference/limits/#std-label-faq-dev-namespace + */ namespace: string; } /** @public */ -export interface ClientInsertOneModel extends ClientWriteModel { +export interface ClientInsertOneModel extends ClientWriteModel { name: 'insertOne'; /** The document to insert. */ - document: OptionalId; + document: OptionalId; } /** @public */ -export interface ClientDeleteOneModel extends ClientWriteModel { +export interface ClientDeleteOneModel extends ClientWriteModel { name: 'deleteOne'; /** * The filter used to determine if a document should be deleted. * For a deleteOne operation, the first match is removed. */ - filter: Filter; + filter: Filter; /** Specifies a collation. */ collation?: CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -53,13 +60,13 @@ export interface ClientDeleteOneModel extends ClientWriteModel { } /** @public */ -export interface ClientDeleteManyModel extends ClientWriteModel { +export interface ClientDeleteManyModel extends ClientWriteModel { name: 'deleteMany'; /** * The filter used to determine if a document should be deleted. * For a deleteMany operation, all matches are removed. */ - filter: Filter; + filter: Filter; /** Specifies a collation. */ collation?: CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -67,15 +74,15 @@ export interface ClientDeleteManyModel extends ClientWriteModel { } /** @public */ -export interface ClientReplaceOneModel extends ClientWriteModel { +export interface ClientReplaceOneModel extends ClientWriteModel { name: 'replaceOne'; /** * The filter used to determine if a document should be replaced. * For a replaceOne operation, the first match is replaced. */ - filter: Filter; + filter: Filter; /** The document with which to replace the matched document. */ - replacement: WithoutId; + replacement: WithoutId; /** Specifies a collation. */ collation?: CollationOptions; /** The index to use. If specified, then the query system will only consider plans using the hinted index. */ @@ -85,19 +92,19 @@ export interface ClientReplaceOneModel extends ClientWriteModel { } /** @public */ -export interface ClientUpdateOneModel extends ClientWriteModel { +export interface ClientUpdateOneModel extends ClientWriteModel { name: 'updateOne'; /** * The filter used to determine if a document should be updated. * For an updateOne operation, the first match is updated. */ - filter: Filter; + filter: Filter; /** * The modifications to apply. The value can be either: * UpdateFilter - A document that contains update operator expressions, * Document[] - an aggregation pipeline. */ - update: UpdateFilter | Document[]; + update: UpdateFilter | Document[]; /** A set of filters specifying to which array elements an update should apply. */ arrayFilters?: Document[]; /** Specifies a collation. */ @@ -109,19 +116,19 @@ export interface ClientUpdateOneModel extends ClientWriteModel { } /** @public */ -export interface ClientUpdateManyModel extends ClientWriteModel { +export interface ClientUpdateManyModel extends ClientWriteModel { name: 'updateMany'; /** * The filter used to determine if a document should be updated. * For an updateMany operation, all matches are updated. */ - filter: Filter; + filter: Filter; /** * The modifications to apply. The value can be either: * UpdateFilter - A document that contains update operator expressions, * Document[] - an aggregation pipeline. */ - update: UpdateFilter | Document[]; + update: UpdateFilter | Document[]; /** A set of filters specifying to which array elements an update should apply. */ arrayFilters?: Document[]; /** Specifies a collation. */ @@ -137,48 +144,81 @@ export interface ClientUpdateManyModel extends ClientWriteModel { * to MongoClient#bulkWrite. * @public */ -export type AnyClientBulkWriteModel = - | ClientInsertOneModel - | ClientReplaceOneModel - | ClientUpdateOneModel - | ClientUpdateManyModel - | ClientDeleteOneModel - | ClientDeleteManyModel; +export type AnyClientBulkWriteModel = + | ClientInsertOneModel + | ClientReplaceOneModel + | ClientUpdateOneModel + | ClientUpdateManyModel + | ClientDeleteOneModel + | ClientDeleteManyModel; + +/** + * A mapping of namespace strings to collections schemas. + * @public + * + * @example + * ```ts + * type MongoDBSchemas = { + * 'db.books': Book; + * 'db.authors': Author; + * } + * + * const model: ClientBulkWriteModel = { + * namespace: 'db.books' + * name: 'insertOne', + * document: { title: 'Practical MongoDB Aggregations', authorName: 3 } // error `authorName` cannot be number + * }; + * ``` + * + * The type of the `namespace` field narrows other parts of the BulkWriteModel to use the correct schema for type assertions. + * + */ +export type ClientBulkWriteModel< + SchemaMap extends Record = Record +> = { + [Namespace in keyof SchemaMap]: AnyClientBulkWriteModel & { + namespace: Namespace; + }; +}[keyof SchemaMap]; /** @public */ export interface ClientBulkWriteResult { + /** + * Whether the bulk write was acknowledged. + */ + readonly acknowledged: boolean; /** * The total number of documents inserted across all insert operations. */ - insertedCount: number; + readonly insertedCount: number; /** * The total number of documents upserted across all update operations. */ - upsertedCount: number; + readonly upsertedCount: number; /** * The total number of documents matched across all update operations. */ - matchedCount: number; + readonly matchedCount: number; /** * The total number of documents modified across all update operations. */ - modifiedCount: number; + readonly modifiedCount: number; /** * The total number of documents deleted across all delete operations. */ - deletedCount: number; + readonly deletedCount: number; /** * The results of each individual insert operation that was successfully performed. */ - insertResults?: Map; + readonly insertResults?: ReadonlyMap; /** * The results of each individual update operation that was successfully performed. */ - updateResults?: Map; + readonly updateResults?: ReadonlyMap; /** * The results of each individual delete operation that was successfully performed. */ - deleteResults?: Map; + readonly deleteResults?: ReadonlyMap; } /** @public */ diff --git a/src/operations/client_bulk_write/executor.ts b/src/operations/client_bulk_write/executor.ts index 7475fcdab20..23c2d08f318 100644 --- a/src/operations/client_bulk_write/executor.ts +++ b/src/operations/client_bulk_write/executor.ts @@ -1,3 +1,5 @@ +import { type Document } from 'bson'; + import { ClientBulkWriteCursor } from '../../cursor/client_bulk_write_cursor'; import { MongoClientBulkWriteError, @@ -22,9 +24,9 @@ import { ClientBulkWriteResultsMerger } from './results_merger'; * @internal */ export class ClientBulkWriteExecutor { - client: MongoClient; - options: ClientBulkWriteOptions; - operations: AnyClientBulkWriteModel[]; + private readonly client: MongoClient; + private readonly options: ClientBulkWriteOptions; + private readonly operations: ReadonlyArray>; /** * Instantiate the executor. @@ -34,7 +36,7 @@ export class ClientBulkWriteExecutor { */ constructor( client: MongoClient, - operations: AnyClientBulkWriteModel[], + operations: ReadonlyArray>, options?: ClientBulkWriteOptions ) { if (operations.length === 0) { @@ -75,7 +77,7 @@ export class ClientBulkWriteExecutor { * for each, then merge the results into one. * @returns The result. */ - async execute(): Promise { + async execute(): Promise { // The command builder will take the user provided models and potential split the batch // into multiple commands due to size. const pkFactory = this.client.s.options.pkFactory; @@ -90,7 +92,7 @@ export class ClientBulkWriteExecutor { const operation = new ClientBulkWriteOperation(commandBuilder, this.options); await executeOperation(this.client, operation); } - return { ok: 1 }; + return ClientBulkWriteResultsMerger.unacknowledged(); } else { const resultsMerger = new ClientBulkWriteResultsMerger(this.options); // For each command will will create and exhaust a cursor for the results. @@ -110,7 +112,7 @@ export class ClientBulkWriteExecutor { message: 'Mongo client bulk write encountered an error during execution' }); bulkWriteError.cause = error; - bulkWriteError.partialResult = resultsMerger.result; + bulkWriteError.partialResult = resultsMerger.bulkWriteResult; throw bulkWriteError; } else { // Client side errors are just thrown. @@ -126,11 +128,11 @@ export class ClientBulkWriteExecutor { }); error.writeConcernErrors = resultsMerger.writeConcernErrors; error.writeErrors = resultsMerger.writeErrors; - error.partialResult = resultsMerger.result; + error.partialResult = resultsMerger.bulkWriteResult; throw error; } - return resultsMerger.result; + return resultsMerger.bulkWriteResult; } } } diff --git a/src/operations/client_bulk_write/results_merger.ts b/src/operations/client_bulk_write/results_merger.ts index 8114523fde2..1a0f0a9f36b 100644 --- a/src/operations/client_bulk_write/results_merger.ts +++ b/src/operations/client_bulk_write/results_merger.ts @@ -11,17 +11,78 @@ import { type ClientUpdateResult } from './common'; +/** + * Unacknowledged bulk writes are always the same. + */ +const UNACKNOWLEDGED = { + acknowledged: false, + insertedCount: 0, + upsertedCount: 0, + matchedCount: 0, + modifiedCount: 0, + deletedCount: 0, + insertResults: undefined, + updateResults: undefined, + deleteResults: undefined +}; + +interface ClientBulkWriteResultAccumulation { + /** + * Whether the bulk write was acknowledged. + */ + acknowledged: boolean; + /** + * The total number of documents inserted across all insert operations. + */ + insertedCount: number; + /** + * The total number of documents upserted across all update operations. + */ + upsertedCount: number; + /** + * The total number of documents matched across all update operations. + */ + matchedCount: number; + /** + * The total number of documents modified across all update operations. + */ + modifiedCount: number; + /** + * The total number of documents deleted across all delete operations. + */ + deletedCount: number; + /** + * The results of each individual insert operation that was successfully performed. + */ + insertResults?: Map; + /** + * The results of each individual update operation that was successfully performed. + */ + updateResults?: Map; + /** + * The results of each individual delete operation that was successfully performed. + */ + deleteResults?: Map; +} + /** * Merges client bulk write cursor responses together into a single result. * @internal */ export class ClientBulkWriteResultsMerger { - result: ClientBulkWriteResult; - options: ClientBulkWriteOptions; - currentBatchOffset: number; + private result: ClientBulkWriteResultAccumulation; + private options: ClientBulkWriteOptions; + private currentBatchOffset: number; writeConcernErrors: Document[]; writeErrors: Map; + /** + * @returns The standard unacknowledged bulk write result. + */ + static unacknowledged(): ClientBulkWriteResult { + return UNACKNOWLEDGED; + } + /** * Instantiate the merger. * @param options - The options. @@ -32,6 +93,7 @@ export class ClientBulkWriteResultsMerger { this.writeConcernErrors = []; this.writeErrors = new Map(); this.result = { + acknowledged: true, insertedCount: 0, upsertedCount: 0, matchedCount: 0, @@ -49,6 +111,23 @@ export class ClientBulkWriteResultsMerger { } } + /** + * Get the bulk write result object. + */ + get bulkWriteResult(): ClientBulkWriteResult { + return { + acknowledged: this.result.acknowledged, + insertedCount: this.result.insertedCount, + upsertedCount: this.result.upsertedCount, + matchedCount: this.result.matchedCount, + modifiedCount: this.result.modifiedCount, + deletedCount: this.result.deletedCount, + insertResults: this.result.insertResults, + updateResults: this.result.updateResults, + deleteResults: this.result.deleteResults + }; + } + /** * Merge the results in the cursor to the existing result. * @param currentBatchOffset - The offset index to the original models. diff --git a/test/integration/crud/crud.prose.test.ts b/test/integration/crud/crud.prose.test.ts index 9b5f58cdb4f..8665d69a1f3 100644 --- a/test/integration/crud/crud.prose.test.ts +++ b/test/integration/crud/crud.prose.test.ts @@ -3,9 +3,10 @@ import { once } from 'events'; import { type CommandStartedEvent } from '../../../mongodb'; import { - type AnyClientBulkWriteModel, + type ClientBulkWriteModel, type ClientSession, type Collection, + type Document, MongoBulkWriteError, type MongoClient, MongoClientBulkWriteError, @@ -175,7 +176,7 @@ describe('CRUD Prose Spec Tests', () => { // firstEvent.operationId is equal to secondEvent.operationId. let client: MongoClient; let maxWriteBatchSize; - const models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -188,12 +189,12 @@ describe('CRUD Prose Spec Tests', () => { client.on('commandStarted', filterForCommands('bulkWrite', commands)); commands.length = 0; - Array.from({ length: maxWriteBatchSize + 1 }, () => { - models.push({ + models = Array.from({ length: maxWriteBatchSize + 1 }, () => { + return { namespace: 'db.coll', name: 'insertOne', document: { a: 'b' } - }); + }; }); }); @@ -243,7 +244,7 @@ describe('CRUD Prose Spec Tests', () => { let maxBsonObjectSize; let maxMessageSizeBytes; let numModels; - const models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -258,14 +259,14 @@ describe('CRUD Prose Spec Tests', () => { client.on('commandStarted', filterForCommands('bulkWrite', commands)); commands.length = 0; - Array.from({ length: numModels }, () => { - models.push({ + models = Array.from({ length: numModels }, () => { + return { name: 'insertOne', namespace: 'db.coll', document: { a: 'b'.repeat(maxBsonObjectSize - 500) } - }); + }; }); }); @@ -314,7 +315,7 @@ describe('CRUD Prose Spec Tests', () => { // Assert that two CommandStartedEvents were observed for the bulkWrite command. let client: MongoClient; let maxWriteBatchSize; - const models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -338,12 +339,12 @@ describe('CRUD Prose Spec Tests', () => { client.on('commandStarted', filterForCommands('bulkWrite', commands)); commands.length = 0; - Array.from({ length: maxWriteBatchSize + 1 }, () => { - models.push({ + models = Array.from({ length: maxWriteBatchSize + 1 }, () => { + return { namespace: 'db.coll', name: 'insertOne', document: { a: 'b' } - }); + }; }); }); @@ -382,7 +383,7 @@ describe('CRUD Prose Spec Tests', () => { // Construct a list of write models (referred to as models) with model repeated maxWriteBatchSize + 1 times. let client: MongoClient; let maxWriteBatchSize; - const models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -390,18 +391,18 @@ describe('CRUD Prose Spec Tests', () => { await client.connect(); await client.db('db').collection('coll').drop(); const hello = await client.db('admin').command({ hello: 1 }); - await client.db('db').collection('coll').insertOne({ _id: 1 }); + await client.db('db').collection<{ _id?: number }>('coll').insertOne({ _id: 1 }); maxWriteBatchSize = hello.maxWriteBatchSize; client.on('commandStarted', filterForCommands('bulkWrite', commands)); commands.length = 0; - Array.from({ length: maxWriteBatchSize + 1 }, () => { - models.push({ + models = Array.from({ length: maxWriteBatchSize + 1 }, () => { + return { namespace: 'db.coll', name: 'insertOne', document: { _id: 1 } - }); + }; }); }); @@ -471,7 +472,7 @@ describe('CRUD Prose Spec Tests', () => { // Assert that a CommandStartedEvent was observed for the getMore command. let client: MongoClient; let maxBsonObjectSize; - const models: AnyClientBulkWriteModel[] = []; + const models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -545,7 +546,7 @@ describe('CRUD Prose Spec Tests', () => { let client: MongoClient; let session: ClientSession; let maxBsonObjectSize; - const models: AnyClientBulkWriteModel[] = []; + const models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -632,7 +633,7 @@ describe('CRUD Prose Spec Tests', () => { // Assert that a CommandStartedEvent was observed for the killCursors command. let client: MongoClient; let maxBsonObjectSize; - const models: AnyClientBulkWriteModel[] = []; + const models: ClientBulkWriteModel[] = []; const getMoreCommands: CommandStartedEvent[] = []; const killCursorsCommands: CommandStartedEvent[] = []; @@ -803,7 +804,7 @@ describe('CRUD Prose Spec Tests', () => { let opsBytes; let numModels; let remainderBytes; - let models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -821,12 +822,12 @@ describe('CRUD Prose Spec Tests', () => { commands.length = 0; models = []; - Array.from({ length: numModels }, () => { - models.push({ + models = Array.from({ length: numModels }, () => { + return { namespace: 'db.coll', name: 'insertOne', document: { a: 'b'.repeat(maxBsonObjectSize - 57) } - }); + }; }); if (remainderBytes >= 217) { @@ -859,7 +860,7 @@ describe('CRUD Prose Spec Tests', () => { it('executes in a single batch', { metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } }, async test() { - const sameNamespaceModel: AnyClientBulkWriteModel = { + const sameNamespaceModel: ClientBulkWriteModel = { name: 'insertOne', namespace: 'db.coll', document: { a: 'b' } @@ -896,7 +897,7 @@ describe('CRUD Prose Spec Tests', () => { metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } }, async test() { const namespace = `db.${'c'.repeat(200)}`; - const newNamespaceModel: AnyClientBulkWriteModel = { + const newNamespaceModel: ClientBulkWriteModel = { name: 'insertOne', namespace: namespace, document: { a: 'b' } @@ -950,7 +951,7 @@ describe('CRUD Prose Spec Tests', () => { it('raises a client error', { metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } }, async test() { - const model: AnyClientBulkWriteModel = { + const model: ClientBulkWriteModel = { name: 'insertOne', namespace: 'db.coll', document: { a: 'b'.repeat(maxMessageSizeBytes) } @@ -976,7 +977,7 @@ describe('CRUD Prose Spec Tests', () => { metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } }, async test() { const namespace = `db.${'c'.repeat(maxMessageSizeBytes)}`; - const model: AnyClientBulkWriteModel = { + const model: ClientBulkWriteModel = { name: 'insertOne', namespace: namespace, document: { a: 'b' } @@ -1033,7 +1034,7 @@ describe('CRUD Prose Spec Tests', () => { }); it('raises a client side error', async function () { - const model: AnyClientBulkWriteModel = { + const model: ClientBulkWriteModel = { name: 'insertOne', namespace: 'db.coll', document: { a: 'b' } @@ -1113,7 +1114,7 @@ describe('CRUD Prose Spec Tests', () => { let maxBsonObjectSize; let maxMessageSizeBytes; let numModels; - let models: AnyClientBulkWriteModel[] = []; + let models: ClientBulkWriteModel[] = []; const commands: CommandStartedEvent[] = []; beforeEach(async function () { @@ -1154,7 +1155,7 @@ describe('CRUD Prose Spec Tests', () => { metadata: { requires: { mongodb: '>=8.0.0', serverless: 'forbid' } }, async test() { const result = await client.bulkWrite(models, { ordered: false, writeConcern: { w: 0 } }); - expect(result).to.deep.equal({ ok: 1 }); + expect(result.acknowledged).to.be.false; expect(commands.length).to.equal(2); expect(commands[0].command.ops.length).to.equal(numModels - 1); expect(commands[0].command.writeConcern.w).to.equal(0); diff --git a/test/types/client_bulk_write.test-d.ts b/test/types/client_bulk_write.test-d.ts new file mode 100644 index 00000000000..834b68b19cd --- /dev/null +++ b/test/types/client_bulk_write.test-d.ts @@ -0,0 +1,274 @@ +import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd'; + +import { + type ClientBulkWriteModel, + type ClientDeleteManyModel, + type ClientDeleteOneModel, + type ClientInsertOneModel, + type ClientReplaceOneModel, + type ClientUpdateManyModel, + type ClientUpdateOneModel, + type Document, + type Filter, + type MongoClient, + type OptionalId, + type UpdateFilter, + type UUID, + type WithoutId +} from '../mongodb'; + +declare const client: MongoClient; +type Book = { title: string; released: Date }; +type Author = { name: string; published: number }; +type Store = { _id: UUID }; + +// Baseline check that schema modifies the following fields for each type. +declare const clientInsertOneModel: ClientInsertOneModel; +expectType>(clientInsertOneModel.document); + +declare const clientReplaceOneModel: ClientReplaceOneModel; +expectType>(clientReplaceOneModel.filter); +expectType>(clientReplaceOneModel.replacement); + +declare const clientUpdateOneModel: ClientUpdateOneModel; +expectType>(clientUpdateOneModel.filter); +expectType | Document[]>(clientUpdateOneModel.update); + +declare const clientUpdateManyModel: ClientUpdateManyModel; +expectType>(clientUpdateManyModel.filter); +expectType | Document[]>(clientUpdateManyModel.update); + +declare const clientDeleteOneModel: ClientDeleteOneModel; +expectType>(clientDeleteOneModel.filter); + +declare const clientDeleteManyModel: ClientDeleteManyModel; +expectType>(clientDeleteManyModel.filter); + +client.bulkWrite([]); // empty should always work + +// No schemas - all correct use +client.bulkWrite([ + { + namespace: 'db.authors', + name: 'insertOne', + document: { name: 'bob', published: 2 } + }, + { + namespace: 'db.authors', + name: 'replaceOne', + filter: { name: 'bob' }, + replacement: { name: 'ann', published: 2 } + }, + { + namespace: 'db.authors', + name: 'updateOne', + filter: { name: 'bob', published: 2 }, + update: {} + }, + { + namespace: 'db.authors', + name: 'updateMany', + filter: { name: 'bob', published: 2 }, + update: {} + }, + { namespace: 'db.authors', name: 'deleteOne', filter: {} }, + { namespace: 'db.authors', name: 'deleteMany', filter: {} } +]); + +// No schemas - random namespaces, no type checking +client.bulkWrite([ + { + namespace: 'db.whatever', + name: 'insertOne', + document: { name: 'bob', randomKey: 2 } + }, + { + namespace: 'db.change', + name: 'replaceOne', + filter: { name: 'bob' }, + replacement: { name: 2, published: 2 } + }, + { + namespace: 'db.it', + name: 'updateOne', + filter: { name: 'bob', published: new Date() }, + update: {} + }, + { + namespace: 'db.up', + name: 'updateMany', + filter: { name: 'bob', published: 2 }, + update: {} + }, + { namespace: 'db.random', name: 'deleteOne', filter: {} }, + { namespace: 'db.namespace', name: 'deleteMany', filter: {} } +]); + +// Operation names are still type checked when there is no schema +expectError({ + namespace: 'db.author', + name: 'insertLots', // Not an operation we support + document: { name: 'bob', published: 2 } +}); + +type MongoDBSchemas = { + 'db.books': Book; + 'db.authors': Author; + 'db.stores': Store; +}; + +expectError>({ + namespace: 'db.author', // Unknown namespace! a typo! + name: 'insertOne', + document: { name: 'bob', published: 2 } +}); + +expectError>({ + namespace: 'db.authors', + name: 'insertOne', + document: { name: 'bob', published: '' } // Incorrect type for known field +}); + +expectError>({ + namespace: 'db.authors', + name: 'insertOne', + document: { name: 'bob', publish: 2 } // unknown field! typo! +}); + +// Defined schemas - all correct use +client.bulkWrite([ + { + namespace: 'db.authors', + name: 'insertOne', + document: { name: 'bob', published: 2 } + }, + { + namespace: 'db.authors', + name: 'replaceOne', + filter: { name: 'bob' }, + replacement: { name: 'ann', published: 2 } + }, + { + namespace: 'db.authors', + name: 'updateOne', + filter: { name: 'bob', published: 2 }, + update: {} + }, + { + namespace: 'db.authors', + name: 'updateMany', + filter: { name: 'bob', published: 2 }, + update: {} + }, + { namespace: 'db.authors', name: 'deleteOne', filter: {} }, + { namespace: 'db.authors', name: 'deleteMany', filter: {} } +]); + +// Defined schemas - incorrect use +expectError( + client.bulkWrite([ + { + namespace: 'db.authors', + name: 'insertOne', + document: { name: 'bob', published: '' } // wrong type + } + ]) +); + +expectError( + client.bulkWrite([ + { + namespace: 'db.authors', + name: 'replaceOne', + filter: { name: 'bob' }, + replacement: { name: 'ann', publish: 2 } // key typo + } + ]) +); + +expectError( + client.bulkWrite([ + { + namespace: 'db.blah', // unknown namespace + name: 'updateOne', + filter: { name: 'bob', published: 2 }, + update: {} + } + ]) +); + +expectError( + client.bulkWrite([ + { + namespace: 'db.authors', + name: 'updateManyy', // unknown operation + filter: { name: 'bob', published: 2 }, + update: {} + } + ]) +); + +type MongoDBSchemasWithCalculations = { + // River Books uses star ratings + [key: `river-books.${string}`]: Book & { fiveStarRatings: number }; + // Ocean literature uses thumbs up for ratings + [key: `ocean-literature.${string}`]: Book & { thumbsUp: number }; +}; + +// correct use +client.bulkWrite([ + { + namespace: 'river-books.store0', + name: 'insertOne', + document: { title: 'abc', released: new Date(), fiveStarRatings: 10 } + }, + { + namespace: 'ocean-literature.store0', + name: 'insertOne', + document: { title: 'abc', released: new Date(), thumbsUp: 10 } + } +]); + +// prevented from changing each store's rating system! +expectError( + client.bulkWrite([ + { + namespace: 'river-books.store0', + name: 'insertOne', + document: { title: 'abc', released: new Date(), thumbsUp: 10 } + }, + { + namespace: 'ocean-literature.store0', + name: 'insertOne', + document: { title: 'abc', released: new Date(), fiveStarRatings: 10 } + } + ]) +); + +// Example partial use case: +// I want to make sure I don't mess up any namespaces but I don't want to define schemas: + +type MongoDBNamespaces = 'db.books' | 'db.authors' | 'db.stores'; + +client.bulkWrite<{ [K in MongoDBNamespaces]: Document }>([ + { + namespace: 'db.books', + name: 'insertOne', + document: { title: 'abc', released: 32n, blah_blah: 10 } // wrong type for released does not error + }, + { + namespace: 'db.authors', + name: 'insertOne', + document: { title: 'abc', released: 'yesterday', fiveStarRatings: 10 } + } +]); + +expectError( + client.bulkWrite<{ [K in MongoDBNamespaces]: Document }>([ + { + namespace: 'db.wrongNS', + name: 'insertOne', + document: { title: 'abc', released: new Date(), thumbsUp: 10 } + } + ]) +); diff --git a/test/unit/operations/client_bulk_write/results_merger.test.ts b/test/unit/operations/client_bulk_write/results_merger.test.ts index c9a954e694a..d1ec999d059 100644 --- a/test/unit/operations/client_bulk_write/results_merger.test.ts +++ b/test/unit/operations/client_bulk_write/results_merger.test.ts @@ -45,6 +45,7 @@ describe('ClientBulkWriteResultsMerger', function () { it('initializes the result', function () { expect(resultsMerger.result).to.deep.equal({ + acknowledged: true, insertedCount: 0, upsertedCount: 0, matchedCount: 0,