diff --git a/package-lock.json b/package-lock.json index 9941fe861c..49bab6f295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14348,7 +14348,7 @@ "dev": true }, "node-codesign": { - "version": "github:durran/node-codesign#8f7adaf9889a6be26f4fd60efa610967cfdf8730", + "version": "github:durran/node-codesign#92863a258c2108556c0a33284d26f635729041da", "from": "github:durran/node-codesign", "dev": true, "requires": { diff --git a/packages/i18n/src/locales/en_US.js b/packages/i18n/src/locales/en_US.js index 339126ba17..897254b9aa 100644 --- a/packages/i18n/src/locales/en_US.js +++ b/packages/i18n/src/locales/en_US.js @@ -556,6 +556,16 @@ const translations = { description: 'Returns an interface to access the query plan cache for a collection. The interface provides methods to view and clear the query plan cache.', example: 'db.coll.getPlanCache()' }, + validate: { + link: 'https://docs.mongodb.com/manual/reference/method/db.collection.validate', + description: 'Calls the validate command. Default full value is false', + example: 'db.validate()' + }, + mapReduce: { + link: 'https://docs.mongodb.com/manual/reference/method/db.collection.mapReduce', + description: 'Calls the mapReduce command', + example: 'db.mapReduce(mapFn, reduceFn, options)' + } } } }, diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index b1e7b4c524..aa660a47f7 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -958,5 +958,94 @@ describe('Collection', () => { expect(pc._asPrintable()).to.equal('PlanCache for collection coll1.'); }); }); + describe('validate', () => { + it('calls serviceProvider.runCommand on the collection default', async() => { + serviceProvider.runCommandWithCheck.resolves({ ok: 1 }); + await collection.validate(); + expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith( + database._name, + { + validate: collection._name, + full: false + } + ); + }); + it('calls serviceProvider.runCommand on the collection with options', async() => { + await collection.validate(true); + expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith( + database._name, + { + validate: collection._name, + full: true + } + ); + }); + + it('returns whatever serviceProvider.runCommand returns', async() => { + const expectedResult = { ok: 1 }; + serviceProvider.runCommandWithCheck.resolves(expectedResult); + const result = await collection.validate(); + expect(result).to.deep.equal(expectedResult); + }); + + it('throws if serviceProvider.runCommand rejects', async() => { + const expectedError = new Error(); + serviceProvider.runCommandWithCheck.rejects(expectedError); + const catchedError = await collection.validate() + .catch(e => e); + expect(catchedError).to.equal(expectedError); + }); + }); + describe('mapReduce', () => { + let mapFn; + let reduceFn; + beforeEach(() => { + mapFn = function(): void {}; + reduceFn = function(keyCustId, valuesPrices): any { + return valuesPrices.reduce((t, s) => (t + s)); + }; + }); + it('calls serviceProvider.mapReduce on the collection with js args', async() => { + serviceProvider.runCommandWithCheck.resolves({ ok: 1 }); + await collection.mapReduce(mapFn, reduceFn, { out: 'map_reduce_example' }); + expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith( + database._name, + { + mapReduce: collection._name, + map: mapFn, + reduce: reduceFn, + out: 'map_reduce_example' + } + ); + }); + it('calls serviceProvider.runCommand on the collection with string args', async() => { + serviceProvider.runCommandWithCheck.resolves({ ok: 1 }); + await collection.mapReduce(mapFn.toString(), reduceFn.toString(), { out: 'map_reduce_example' }); + expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith( + database._name, + { + mapReduce: collection._name, + map: mapFn.toString(), + reduce: reduceFn.toString(), + out: 'map_reduce_example' + } + ); + }); + + it('returns whatever serviceProvider.mapReduce returns', async() => { + const expectedResult = { ok: 1 }; + serviceProvider.runCommandWithCheck.resolves(expectedResult); + const result = await collection.mapReduce(mapFn, reduceFn, { out: { inline: 1 } }); + expect(result).to.deep.equal(expectedResult); + }); + + it('throws if serviceProvider.mapReduce rejects', async() => { + const expectedError = new Error(); + serviceProvider.runCommandWithCheck.rejects(expectedError); + const catchedError = await collection.mapReduce(mapFn, reduceFn, { out: { inline: 1 } }) + .catch(e => e); + expect(catchedError).to.equal(expectedError); + }); + }); }); }); diff --git a/packages/shell-api/src/collection.ts b/packages/shell-api/src/collection.ts index 2dadcaf9ce..d15a7f8096 100644 --- a/packages/shell-api/src/collection.ts +++ b/packages/shell-api/src/collection.ts @@ -1364,4 +1364,41 @@ export default class Collection extends ShellApiClass { this._emitCollectionApiCall('getPlanCache'); return new PlanCache(this); } + + @returnsPromise + async mapReduce(map: any, reduce: any, optionsOrOutString: Document | string): Promise { + assertArgsDefined(map, reduce, optionsOrOutString); + this._emitCollectionApiCall('mapReduce', { map, reduce, out: optionsOrOutString }); + + let cmd = { + mapReduce: this._name, + map: map, + reduce: reduce + } as any; + + if (typeof optionsOrOutString === 'string') { + cmd.out = optionsOrOutString; + } else if (optionsOrOutString.out === undefined) { + throw new MongoshInvalidInputError('Missing \'out\' option'); + } else { + cmd = { ...cmd, ...optionsOrOutString }; + } + + return await this._mongo._serviceProvider.runCommandWithCheck( + this._database._name, + cmd + ); + } + + @returnsPromise + async validate(full = false): Promise { + this._emitCollectionApiCall('validate', { full }); + return await this._mongo._serviceProvider.runCommandWithCheck( + this._database._name, + { + validate: this._name, + full: full + } + ); + } } diff --git a/packages/shell-api/src/integration.spec.ts b/packages/shell-api/src/integration.spec.ts index 32bd0b48df..848f8eef64 100644 --- a/packages/shell-api/src/integration.spec.ts +++ b/packages/shell-api/src/integration.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/camelcase */ import { expect } from 'chai'; import { CliServiceProvider } from '../../service-provider-server'; // avoid cyclic dep just for test import ShellInternalState from './shell-internal-state'; @@ -66,6 +67,22 @@ describe('Shell API (integration)', function() { await collection.find( { quantity: { $gte: 5 }, type: 'apparel' } ).toArray(); }; + const loadMRExample = async(collection): Promise => { + const res = await collection.insertMany([ + { _id: 1, cust_id: 'Ant O. Knee', ord_date: new Date('2020-03-01'), price: 25, items: [ { sku: 'oranges', qty: 5, price: 2.5 }, { sku: 'apples', qty: 5, price: 2.5 } ], status: 'A' }, + { _id: 2, cust_id: 'Ant O. Knee', ord_date: new Date('2020-03-08'), price: 70, items: [ { sku: 'oranges', qty: 8, price: 2.5 }, { sku: 'chocolates', qty: 5, price: 10 } ], status: 'A' }, + { _id: 3, cust_id: 'Busby Bee', ord_date: new Date('2020-03-08'), price: 50, items: [ { sku: 'oranges', qty: 10, price: 2.5 }, { sku: 'pears', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 4, cust_id: 'Busby Bee', ord_date: new Date('2020-03-18'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 5, cust_id: 'Busby Bee', ord_date: new Date('2020-03-19'), price: 50, items: [ { sku: 'chocolates', qty: 5, price: 10 } ], status: 'A' }, + { _id: 6, cust_id: 'Cam Elot', ord_date: new Date('2020-03-19'), price: 35, items: [ { sku: 'carrots', qty: 10, price: 1.0 }, { sku: 'apples', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 7, cust_id: 'Cam Elot', ord_date: new Date('2020-03-20'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 8, cust_id: 'Don Quis', ord_date: new Date('2020-03-20'), price: 75, items: [ { sku: 'chocolates', qty: 5, price: 10 }, { sku: 'apples', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 9, cust_id: 'Don Quis', ord_date: new Date('2020-03-20'), price: 55, items: [ { sku: 'carrots', qty: 5, price: 1.0 }, { sku: 'apples', qty: 10, price: 2.5 }, { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' }, + { _id: 10, cust_id: 'Don Quis', ord_date: new Date('2020-03-23'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' } + ]); + expect(res.acknowledged).to.equal(1); + }; + before(async() => { serviceProvider = await CliServiceProvider.connect(connectionString); }); @@ -1348,4 +1365,92 @@ describe('Shell API (integration)', function() { }); }); }); + describe('mapReduce', () => { + it('accepts function args and collection name as string', async() => { + await loadMRExample(collection); + const mapFn = `function() { + emit(this.cust_id, this.price); + };`; + const reduceFn = function(keyCustId, valuesPrices): any { + return valuesPrices.reduce((s, t) => s + t); + }; + const result = await collection.mapReduce(mapFn, reduceFn, 'map_reduce_example'); + expect(result.ok).to.equal(1); + const outRes = await database.map_reduce_example.find().sort({ _id: 1 }).toArray(); + expect(outRes).to.deep.equal([ + { '_id': 'Ant O. Knee', 'value': 95 }, + { '_id': 'Busby Bee', 'value': 125 }, + { '_id': 'Cam Elot', 'value': 60 }, + { '_id': 'Don Quis', 'value': 155 } + ]); + }); + it('accepts string args and collection name as string', async() => { + await loadMRExample(collection); + const mapFn = `function() { + emit(this.cust_id, this.price); + };`; + const reduceFn = function(keyCustId, valuesPrices): any { + return valuesPrices.reduce((s, t) => s + t); + }; + const result = await collection.mapReduce(mapFn, reduceFn.toString(), 'map_reduce_example'); + expect(result.ok).to.equal(1); + expect(result.result).to.equal('map_reduce_example'); + const outRes = await database.map_reduce_example.find().sort({ _id: 1 }).toArray(); + expect(outRes).to.deep.equal([ + { '_id': 'Ant O. Knee', 'value': 95 }, + { '_id': 'Busby Bee', 'value': 125 }, + { '_id': 'Cam Elot', 'value': 60 }, + { '_id': 'Don Quis', 'value': 155 } + ]); + }); + it('accepts inline as option', async() => { + await loadMRExample(collection); + const mapFn = `function() { + emit(this.cust_id, this.price); + };`; + const reduceFn = function(keyCustId, valuesPrices): any { + return valuesPrices.reduce((s, t) => s + t); + }; + const result = await collection.mapReduce(mapFn, reduceFn.toString(), { + out: { inline: 1 } + }); + expect(result.ok).to.equal(1); + expect(result.results.map(k => k._id).sort()).to.deep.equal([ + 'Ant O. Knee', + 'Busby Bee', + 'Cam Elot', + 'Don Quis' + ]); + expect(result.results.map(k => k.value).sort()).to.deep.equal([ + 125, + 155, + 60, + 95 + ]); + }); + it('accepts finalize as option', async() => { + await loadMRExample(collection); + const mapFn = `function() { + emit(this.cust_id, this.price); + };`; + const reduceFn = function(keyCustId, valuesPrices): any { + return valuesPrices.reduce((s, t) => s + t); + }; + const finalizeFn = function(): any { + return 1; + }; + const result = await collection.mapReduce(mapFn, reduceFn.toString(), { + out: { inline: 1 }, + finalize: finalizeFn + }); + expect(result.ok).to.equal(1); + expect(result.results.map(k => k._id).sort()).to.deep.equal([ + 'Ant O. Knee', + 'Busby Bee', + 'Cam Elot', + 'Don Quis' + ]); + expect(result.results.map(k => k.value)).to.deep.equal([1, 1, 1, 1]); + }); + }); });