Skip to content

Commit

Permalink
Merge pull request reactioncommerce#6588 from reactioncommerce/feat/u…
Browse files Browse the repository at this point in the history
…pgrade-mongodb

feat: upgrade mongodb to 4.4
  • Loading branch information
brent-hoover authored Dec 19, 2022
2 parents 924e7f1 + 189a6f8 commit 61424f1
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 86 deletions.
6 changes: 6 additions & 0 deletions .changeset/sweet-dolphins-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@reactioncommerce/api-core": minor
"@reactioncommerce/file-collections-sa-gridfs": minor
---

feat: upgrade mongodb to 4.4
2 changes: 1 addition & 1 deletion packages/api-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"express": "^4.17.1",
"graphql-iso-date": "^3.6.1",
"lodash": "^4.17.15",
"mongodb": "3.6.2",
"mongodb": "4.4.1",
"promise-retry": "^1.1.1",
"simpl-schema": "^1.7.0"
},
Expand Down
148 changes: 110 additions & 38 deletions packages/api-core/src/ReactionAPICore.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable no-extra-bind */
import { createServer } from "http";
import { createRequire } from "module";
import diehard from "diehard";
import express from "express";
import _ from "lodash";
import mongodb from "mongodb";
import * as mongodb from "mongodb";
import SimpleSchema from "simpl-schema";
import collectionIndex from "@reactioncommerce/api-utils/collectionIndex.js";
import getAbsoluteUrl from "@reactioncommerce/api-utils/getAbsoluteUrl.js";
Expand Down Expand Up @@ -276,43 +277,9 @@ export default class ReactionAPICore {
// Add the collection instance to `context.collections`.
// If the collection already exists, we need to modify it instead of calling
// `createCollection`, in order to add validation options.
const getCollectionPromise = new Promise((resolve, reject) => {
this.db.collection(
collectionConfig.name,
{ strict: true },
(error, collection) => {
if (error) {
// Collection with this name doesn't yet exist
this.db
.createCollection(
collectionConfig.name,
collectionOptions
)
.then((newCollection) => {
resolve(newCollection);
return null;
})
.catch(reject);
} else {
// Collection with this name exists, so modify before resolving
this.db
.command({
collMod: collectionConfig.name,
...collectionOptions
})
.then(() => {
resolve(collection);
return null;
})
.catch(reject);
}
}
);
});

/* eslint-enable promise/no-promise-in-callback */

this.collections[collectionKey] = await getCollectionPromise; // eslint-disable-line no-await-in-loop
// eslint-disable-next-line no-await-in-loop
this.collections[collectionKey] = await this.getCollection(collectionConfig, collectionOptions);
this.applyMongoV3BackwardCompatible(this.collections[collectionKey]);

// If the collection config has `indexes` key, define all requested indexes
if (Array.isArray(collectionConfig.indexes)) {
Expand All @@ -330,6 +297,111 @@ export default class ReactionAPICore {
}
}

/**
* @summary Get legacy collection object
* @param {Object} collectionConfig - The collection config
* @param {Object} collectionOptions - The collection options
* @returns {Object} - legacy collection
*/
async getCollection(collectionConfig, collectionOptions) {
try {
return this.db.collection(collectionConfig.name, collectionOptions);
} catch {
return this.db
.command({ collMod: collectionConfig.name, ...collectionOptions });
}
}

/**
* @summary Apply MongoV3 backward compatible
* @param {Object} collection - The legacy collection object
* @returns {undefined} Nothing
*/
applyMongoV3BackwardCompatible(collection) {
const prevFind = collection.find.bind(collection);
collection.find = ((...args) => {
const result = prevFind(...args);
result.cmd = { query: result[Reflect.ownKeys(result).find((symbol) => String(symbol) === "Symbol(filter)")] };
result.options = { db: collection.s.db };
result.ns = `${collection.s.namespace.db}.${collection.s.namespace.collection}`;
return result;
}).bind(collection);

const acknowledgedToOk = (acknowledged) => (acknowledged ? 1 : 0);

const prevDeleteOne = collection.deleteOne.bind(collection);
collection.deleteOne = (async (...args) => {
const response = await prevDeleteOne(...args);
// eslint-disable-next-line id-length
return { ...response, result: { n: response.deletedCount, ok: acknowledgedToOk(response.acknowledged) } };
}).bind(collection);

collection.removeOne = collection.deleteOne.bind(collection);

const prevUpdateMany = collection.updateMany.bind(collection);
collection.updateMany = (async (...args) => {
const response = await prevUpdateMany(...args);
// eslint-disable-next-line id-length
return { ...response, result: { n: response.modifiedCount, ok: acknowledgedToOk(response.acknowledged) } };
}).bind(collection);

const prevInsertOne = collection.insertOne.bind(collection);
collection.insertOne = (async (...args) => {
const response = await prevInsertOne(...args);
// eslint-disable-next-line id-length
return { ...response, result: { n: response.acknowledged ? 1 : 0, ok: acknowledgedToOk(response.acknowledged) } };
}).bind(collection);

const prevFindOneAndUpdate = collection.findOneAndUpdate.bind(collection);
collection.findOneAndUpdate = (async (...args) => {
const options = args[2];
if (options && typeof options.returnOriginal !== "undefined") {
args[2].returnDocument = options.returnOriginal ? mongodb.ReturnDocument.BEFORE : mongodb.ReturnDocument.AFTER;
}
const response = await prevFindOneAndUpdate(...args);
return { ...response, modifiedCount: response.lastErrorObject.n };
}).bind(collection);

const prevReplaceOne = collection.replaceOne.bind(collection);
collection.replaceOne = (async (...args) => {
const response = await prevReplaceOne(...args);
// eslint-disable-next-line id-length
return { ...response, result: { n: response.modifiedCount, ok: acknowledgedToOk(response.acknowledged) } };
}).bind(collection);

const prevUpdateOne = collection.updateOne.bind(collection);
collection.updateOne = (async (...args) => {
const response = await prevUpdateOne(...args);
// eslint-disable-next-line id-length
return { ...response, result: { n: response.modifiedCount, ok: acknowledgedToOk(response.acknowledged) } };
}).bind(collection);

const prevBulkWrite = collection.bulkWrite.bind(collection);
collection.bulkWrite = (async (...args) => {
const response = await prevBulkWrite(...args);
const {
nInserted,
nUpserted,
nMatched,
nModified,
nRemoved
} = response.result;
return {
...response,
nInserted,
nUpserted,
nMatched,
nModified,
nRemoved,
insertedCount: nInserted,
matchedCount: nMatched,
modifiedCount: nModified,
deletedCount: nRemoved,
upsertedCount: nUpserted
};
}).bind(collection);
}

/**
* @summary Given a MongoDB URL, creates a connection to it, sets `this.mongoClient`,
* calls `this.setMongoDatabase` with the database instance, and then
Expand Down
88 changes: 88 additions & 0 deletions packages/api-core/src/ReactionAPICore.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable id-length */

import importAsString from "@reactioncommerce/api-utils/importAsString.js";
import ReactionAPICore from "./ReactionAPICore.js";
import appEvents from "./util/appEvents.js";
Expand Down Expand Up @@ -57,3 +59,89 @@ test("throws error if appEvents is missing any props", () => {
expect(error.message).toBe("appEvents is missing the following required function properties: emit, on, resume, stop");
}
});

test("getCollection should return correct values", async () => {
const collectionConfig = { name: "Test" };
const mockCollection = {
find: jest.fn().mockReturnValue({}),
deleteOne: jest.fn().mockReturnValue({ deletedCount: 1, acknowledged: true }),
updateMany: jest.fn().mockReturnValue({ modifiedCount: 2, acknowledged: true }),
insertOne: jest.fn().mockReturnValue({ acknowledged: true }),
findOneAndUpdate: jest.fn().mockReturnValue({ ok: 1, lastErrorObject: { n: 1 } }),
replaceOne: jest.fn().mockReturnValue({ modifiedCount: 1, acknowledged: true }),
updateOne: jest.fn().mockReturnValue({ modifiedCount: 1, acknowledged: true }),
bulkWrite: jest.fn().mockReturnValue({
result: {
ok: 1,
writeErrors: [],
writeConcernErrors: [],
nInserted: 1,
nUpserted: 2,
nMatched: 3,
nModified: 4,
nRemoved: 5
}
}),
// eslint-disable-next-line id-length
s: {
db: jest.fn(),
namespace: {
db: "test_db",
collection: "test_collection"
}
}
};

const api = new ReactionAPICore();

api.db = {
collection: () => ({ ...mockCollection }),
command: jest.fn()
};

const collection = await api.getCollection(collectionConfig, {});
api.applyMongoV3BackwardCompatible(collection);

collection.find({});
expect(mockCollection.find).toBeCalled();

const deleteOneResult = await collection.deleteOne({});
expect(mockCollection.deleteOne).toBeCalled();
expect(deleteOneResult).toEqual({ ...deleteOneResult, result: { n: 1, ok: 1 } });

const updateManyResult = await collection.updateMany({});
expect(mockCollection.updateMany).toBeCalled();
expect(updateManyResult).toEqual({ ...updateManyResult, result: { n: 2, ok: 1 } });

const insertOneResult = await collection.insertOne({});
expect(mockCollection.insertOne).toBeCalled();
expect(insertOneResult).toEqual({ ...insertOneResult, result: { n: 1, ok: 1 } });

const findOneAndUpdateResult = await collection.findOneAndUpdate({});
expect(mockCollection.findOneAndUpdate).toBeCalled();
expect(findOneAndUpdateResult).toEqual({ ...findOneAndUpdateResult, modifiedCount: 1 });

const replaceOneResult = await collection.replaceOne({});
expect(mockCollection.replaceOne).toBeCalled();
expect(replaceOneResult).toEqual({ ...replaceOneResult, result: { n: 1, ok: 1 } });

const updateOneResult = await collection.updateOne({});
expect(mockCollection.updateOne).toBeCalled();
expect(updateOneResult).toEqual({ ...updateOneResult, result: { n: 1, ok: 1 } });

const bulkWriteResult = await collection.bulkWrite({});
expect(mockCollection.updateOne).toBeCalled();
expect(bulkWriteResult).toEqual({
...bulkWriteResult,
nInserted: 1,
nUpserted: 2,
nMatched: 3,
nModified: 4,
nRemoved: 5,
insertedCount: 1,
upsertedCount: 2,
matchedCount: 3,
modifiedCount: 4,
deletedCount: 5
});
});
2 changes: 1 addition & 1 deletion packages/api-core/src/util/mongoConnectWithRetry.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Logger from "@reactioncommerce/logger";
import mongodb from "mongodb";
import * as mongodb from "mongodb";
import promiseRetry from "promise-retry";
import config from "../config.js";

Expand Down
6 changes: 4 additions & 2 deletions packages/file-collections-sa-gridfs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
"dependencies": {
"@babel/runtime-corejs2": "^7.14.8",
"@reactioncommerce/file-collections-sa-base": "^0.2.2",
"debug": "^4.3.2",
"gridfs-stream": "^1.1.1"
"debug": "^4.3.2"
},
"devDependencies": {
"@babel/cli": "^7.10.5",
Expand All @@ -87,6 +86,9 @@
"@babel/preset-env": "^7.10.4",
"babel-core": "^7.0.0-bridge.0"
},
"peerDependencies": {
"mongodb": ">= 4.4.1 < 5"
},
"publishConfig": {
"access": "public"
}
Expand Down
26 changes: 11 additions & 15 deletions packages/file-collections-sa-gridfs/src/GridFSStore.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Grid from "gridfs-stream";
import StorageAdapter from "@reactioncommerce/file-collections-sa-base";
import debug from "./debug";

Expand All @@ -25,7 +24,7 @@ export default class GridFSStore extends StorageAdapter {

this.chunkSize = chunkSize;
this.collectionName = `${collectionPrefix}${name}`.trim();
this.grid = Grid(db, mongodb);
this.grid = new mongodb.GridFSBucket(db);
this.mongodb = mongodb;
}

Expand All @@ -42,37 +41,34 @@ export default class GridFSStore extends StorageAdapter {
}

_getReadStream(fileKey, { start: startPos, end: endPos } = {}) {
const opts = { _id: fileKey._id, root: this.collectionName };
const opts = {};

// Add range if this should be a partial read
if (typeof startPos === "number" && typeof endPos === "number") {
opts.range = { startPos, endPos };
opts.start = startPos;
opts.end = endPos;
}

debug("GridFSStore _getReadStream opts:", opts);

return this.grid.createReadStream(opts);
const _id = new this.mongodb.ObjectId(fileKey._id);
return this.grid.openDownloadStream(_id, opts);
}

_getWriteStream(fileKey, options = {}) {
const opts = {
chunk_size: this.chunkSize, // eslint-disable-line camelcase
content_type: "application/octet-stream", // eslint-disable-line camelcase
filename: fileKey.filename,
mode: "w", // overwrite any existing data
root: this.collectionName,
chunkSizeBytes: this.chunkSize,
contentType: "application/octet-stream",
...options
};

if (fileKey._id) opts._id = fileKey._id;

debug("GridFSStore _getWriteStream opts:", opts);

const writeStream = this.grid.createWriteStream(opts);
const writeStream = this.grid.openUploadStream(fileKey.filename, opts);

writeStream.on("close", (file) => {
writeStream.on("finish", (file) => {
if (!file) {
// gridfs-stream will emit "close" without passing a file
// gridfs will emit "finish" without passing a file
// if there is an error. We can simply exit here because
// the "error" listener will also be called in this case.
return;
Expand Down
Loading

0 comments on commit 61424f1

Please sign in to comment.