diff --git a/packages/server/__mocks__/mongodb.ts b/packages/server/__mocks__/mongodb.ts deleted file mode 100644 index 659d35f5b3..0000000000 --- a/packages/server/__mocks__/mongodb.ts +++ /dev/null @@ -1,40 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -module MongoMock { - const mongodb: any = {} - - mongodb.MongoClient = function () { - this.connect = jest.fn() - this.close = jest.fn() - this.insertOne = jest.fn() - this.insertMany = jest.fn(() => ({ toArray: () => [] })) - this.find = jest.fn(() => ({ toArray: () => [] })) - this.findOne = jest.fn() - this.findOneAndUpdate = jest.fn() - this.count = jest.fn() - this.deleteOne = jest.fn() - this.deleteMany = jest.fn(() => ({ toArray: () => [] })) - this.updateOne = jest.fn() - this.updateMany = jest.fn(() => ({ toArray: () => [] })) - - this.collection = jest.fn(() => ({ - insertOne: this.insertOne, - find: this.find, - insertMany: this.insertMany, - findOne: this.findOne, - findOneAndUpdate: this.findOneAndUpdate, - count: this.count, - deleteOne: this.deleteOne, - deleteMany: this.deleteMany, - updateOne: this.updateOne, - updateMany: this.updateMany, - })) - - this.db = () => ({ - collection: this.collection, - }) - } - - mongodb.ObjectId = jest.requireActual("mongodb").ObjectId - - module.exports = mongodb -} diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index b61f905bef..e0351e0ce3 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -3,8 +3,6 @@ import * as setup from "../utilities" import { databaseTestProviders } from "../../../../integrations/tests/utils" import { MongoClient, type Collection, BSON } from "mongodb" -jest.unmock("mongodb") - const collection = "test_collection" const expectValidId = expect.stringMatching(/^\w{24}$/) @@ -36,27 +34,27 @@ describe("/queries", () => { return await config.api.query.create(combinedQuery) } - async function withClient( - callback: (client: MongoClient) => Promise - ): Promise { + async function withClient( + callback: (client: MongoClient) => Promise + ): Promise { const ds = await databaseTestProviders.mongodb.datasource() const client = new MongoClient(ds.config!.connectionString) await client.connect() try { - await callback(client) + return await callback(client) } finally { await client.close() } } - async function withCollection( - callback: (collection: Collection) => Promise - ): Promise { - await withClient(async client => { + async function withCollection( + callback: (collection: Collection) => Promise + ): Promise { + return await withClient(async client => { const db = client.db( (await databaseTestProviders.mongodb.datasource()).config!.db ) - await callback(db.collection(collection)) + return await callback(db.collection(collection)) }) } @@ -327,6 +325,42 @@ describe("/queries", () => { }) }) + it("should be able to updateOne by ObjectId", async () => { + const insertResult = await withCollection(c => c.insertOne({ name: "one" })) + const query = await createQuery({ + fields: { + json: { + filter: { _id: { $eq: `ObjectId("${insertResult.insertedId}")` } }, + update: { $set: { name: "newName" } }, + }, + extra: { + actionType: "updateOne", + }, + }, + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + acknowledged: true, + matchedCount: 1, + modifiedCount: 1, + upsertedCount: 0, + upsertedId: null, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ name: { $eq: "newName" } }) + expect(doc).toEqual({ + _id: insertResult.insertedId, + name: "newName", + }) + }) + }) + it("should be able to delete all records", async () => { const query = await createQuery({ fields: { @@ -390,4 +424,85 @@ describe("/queries", () => { } }) }) + + it("should throw an error if the incorrect actionType is specified", async () => { + const verbs = ["read", "create", "update", "delete"] + for (const verb of verbs) { + const query = await createQuery({ + fields: { json: {}, extra: { actionType: "invalid" } }, + queryVerb: verb, + }) + await config.api.query.execute(query._id!, undefined, { status: 400 }) + } + }) + + it("should ignore extra brackets in query", async () => { + const query = await createQuery({ + fields: { + json: { foo: "te}st" }, + extra: { + actionType: "insertOne", + }, + }, + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!) + expect(result.data).toEqual([ + { + acknowledged: true, + insertedId: expectValidId, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ foo: { $eq: "te}st" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + foo: "te}st", + }) + }) + }) + + it("should ignore be able to save deeply nested data", async () => { + const data = { + foo: "bar", + data: [ + { cid: 1 }, + { cid: 2 }, + { + nested: { + name: "test", + ary: [1, 2, 3], + aryOfObjects: [{ a: 1 }, { b: 2 }], + }, + }, + ], + } + const query = await createQuery({ + fields: { + json: data, + extra: { + actionType: "insertOne", + }, + }, + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!) + expect(result.data).toEqual([ + { + acknowledged: true, + insertedId: expectValidId, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ foo: { $eq: "bar" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + ...data, + }) + }) + }) }) diff --git a/packages/server/src/integrations/mongodb.ts b/packages/server/src/integrations/mongodb.ts index 272810b052..c9852e4c7a 100644 --- a/packages/server/src/integrations/mongodb.ts +++ b/packages/server/src/integrations/mongodb.ts @@ -23,7 +23,7 @@ import { } from "mongodb" import environment from "../environment" -interface MongoDBConfig { +export interface MongoDBConfig { connectionString: string db: string tlsCertificateKeyFile: string @@ -348,7 +348,7 @@ const getSchema = () => { const SCHEMA: Integration = getSchema() -class MongoIntegration implements IntegrationBase { +export class MongoIntegration implements IntegrationBase { private config: MongoDBConfig private client: MongoClient diff --git a/packages/server/src/integrations/tests/mongo.spec.ts b/packages/server/src/integrations/tests/mongo.spec.ts deleted file mode 100644 index 74dd9d4ec0..0000000000 --- a/packages/server/src/integrations/tests/mongo.spec.ts +++ /dev/null @@ -1,325 +0,0 @@ -const mongo = require("mongodb") - -import { default as MongoDBIntegration } from "../mongodb" - -jest.mock("mongodb") - -class TestConfiguration { - integration: any - - constructor(config: any = {}) { - this.integration = new MongoDBIntegration.integration(config) - } -} - -describe("MongoDB Integration", () => { - let config: any - let indexName = "Users" - - beforeEach(() => { - config = new TestConfiguration() - }) - - it("calls the create method with the correct params", async () => { - const body = { - name: "Hello", - } - await config.integration.create({ - index: indexName, - json: body, - extra: { collection: "testCollection", actionType: "insertOne" }, - }) - expect(config.integration.client.insertOne).toHaveBeenCalledWith(body) - }) - - it("calls the read method with the correct params", async () => { - const query = { - json: { - address: "test", - }, - extra: { collection: "testCollection", actionType: "find" }, - } - const response = await config.integration.read(query) - expect(config.integration.client.find).toHaveBeenCalledWith(query.json) - expect(response).toEqual(expect.any(Array)) - }) - - it("calls the delete method with the correct params", async () => { - const query = { - json: { - filter: { - id: "test", - }, - options: { - opt: "option", - }, - }, - extra: { collection: "testCollection", actionType: "deleteOne" }, - } - await config.integration.delete(query) - expect(config.integration.client.deleteOne).toHaveBeenCalledWith( - query.json.filter, - query.json.options - ) - }) - - it("calls the update method with the correct params", async () => { - const query = { - json: { - filter: { - id: "test", - }, - update: { - name: "TestName", - }, - options: { - upsert: false, - }, - }, - extra: { collection: "testCollection", actionType: "updateOne" }, - } - await config.integration.update(query) - expect(config.integration.client.updateOne).toHaveBeenCalledWith( - query.json.filter, - query.json.update, - query.json.options - ) - }) - - it("throws an error when an invalid query.extra.actionType is passed for each method", async () => { - const query = { - extra: { collection: "testCollection", actionType: "deleteOne" }, - } - - let error = null - try { - await config.integration.read(query) - } catch (err) { - error = err - } - expect(error).toBeDefined() - }) - - it("creates ObjectIds if the field contains a match on ObjectId", async () => { - const query = { - json: { - filter: { - _id: "ObjectId('ACBD12345678ABCD12345678')", - name: "ObjectId('BBBB12345678ABCD12345678')", - }, - update: { - _id: "ObjectId('FFFF12345678ABCD12345678')", - name: "ObjectId('CCCC12345678ABCD12345678')", - }, - options: { - upsert: false, - }, - }, - extra: { collection: "testCollection", actionType: "updateOne" }, - } - await config.integration.update(query) - expect(config.integration.client.updateOne).toHaveBeenCalled() - - const args = config.integration.client.updateOne.mock.calls[0] - expect(args[0]).toEqual({ - _id: mongo.ObjectId.createFromHexString("ACBD12345678ABCD12345678"), - name: mongo.ObjectId.createFromHexString("BBBB12345678ABCD12345678"), - }) - expect(args[1]).toEqual({ - _id: mongo.ObjectId.createFromHexString("FFFF12345678ABCD12345678"), - name: mongo.ObjectId.createFromHexString("CCCC12345678ABCD12345678"), - }) - expect(args[2]).toEqual({ - upsert: false, - }) - }) - - it("creates ObjectIds if the $ operator fields contains a match on ObjectId", async () => { - const query = { - json: { - filter: { - _id: { - $eq: "ObjectId('ACBD12345678ABCD12345678')", - }, - }, - update: { - $set: { - _id: "ObjectId('FFFF12345678ABCD12345678')", - }, - }, - options: { - upsert: true, - }, - }, - extra: { collection: "testCollection", actionType: "updateOne" }, - } - await config.integration.update(query) - expect(config.integration.client.updateOne).toHaveBeenCalled() - - const args = config.integration.client.updateOne.mock.calls[0] - expect(args[0]).toEqual({ - _id: { - $eq: mongo.ObjectId.createFromHexString("ACBD12345678ABCD12345678"), - }, - }) - expect(args[1]).toEqual({ - $set: { - _id: mongo.ObjectId.createFromHexString("FFFF12345678ABCD12345678"), - }, - }) - expect(args[2]).toEqual({ - upsert: true, - }) - }) - - it("supports findOneAndUpdate", async () => { - const query = { - json: { - filter: { - _id: { - $eq: "ObjectId('ACBD12345678ABCD12345678')", - }, - }, - update: { - $set: { - name: "UPDATED", - age: 99, - }, - }, - options: { - upsert: false, - }, - }, - extra: { collection: "testCollection", actionType: "findOneAndUpdate" }, - } - await config.integration.read(query) - expect(config.integration.client.findOneAndUpdate).toHaveBeenCalled() - - const args = config.integration.client.findOneAndUpdate.mock.calls[0] - expect(args[0]).toEqual({ - _id: { - $eq: mongo.ObjectId.createFromHexString("ACBD12345678ABCD12345678"), - }, - }) - expect(args[1]).toEqual({ - $set: { - name: "UPDATED", - age: 99, - }, - }) - expect(args[2]).toEqual({ - upsert: false, - includeResultMetadata: true, - }) - }) - - it("can parse nested objects with arrays", async () => { - const query = { - json: `{ - "_id": { - "$eq": "ObjectId('ACBD12345678ABCD12345678')" - } - }, - { - "$set": { - "value": { - "data": [ - { "cid": 1 }, - { "cid": 2 }, - { "nested": { - "name": "test" - }} - ] - } - } - }, - { - "upsert": true - }`, - extra: { collection: "testCollection", actionType: "updateOne" }, - } - await config.integration.update(query) - expect(config.integration.client.updateOne).toHaveBeenCalled() - - const args = config.integration.client.updateOne.mock.calls[0] - expect(args[0]).toEqual({ - _id: { - $eq: mongo.ObjectId.createFromHexString("ACBD12345678ABCD12345678"), - }, - }) - expect(args[1]).toEqual({ - $set: { - value: { - data: [ - { cid: 1 }, - { cid: 2 }, - { - nested: { - name: "test", - }, - }, - ], - }, - }, - }) - expect(args[2]).toEqual({ - upsert: true, - }) - }) - - it("ignores braces within strings when parsing nested objects", async () => { - const query = { - json: `{ - "_id": { - "$eq": "ObjectId('ACBD12345678ABCD12345678')" - } - }, - { - "$set": { - "value": { - "data": [ - { "cid": 1 }, - { "cid": 2 }, - { "nested": { - "name": "te}st" - }} - ] - } - } - }, - { - "upsert": true, - "extra": "ad\\"{\\"d" - }`, - extra: { collection: "testCollection", actionType: "updateOne" }, - } - await config.integration.update(query) - expect(config.integration.client.updateOne).toHaveBeenCalled() - - const args = config.integration.client.updateOne.mock.calls[0] - expect(args[0]).toEqual({ - _id: { - $eq: mongo.ObjectId.createFromHexString("ACBD12345678ABCD12345678"), - }, - }) - expect(args[1]).toEqual({ - $set: { - value: { - data: [ - { cid: 1 }, - { cid: 2 }, - { - nested: { - name: "te}st", - }, - }, - ], - }, - }, - }) - expect(args[2]).toEqual({ - upsert: true, - extra: 'ad"{"d', - }) - }) -}) diff --git a/packages/server/src/tests/utilities/api/query.ts b/packages/server/src/tests/utilities/api/query.ts index 32866314ff..7b887c3fb6 100644 --- a/packages/server/src/tests/utilities/api/query.ts +++ b/packages/server/src/tests/utilities/api/query.ts @@ -5,7 +5,7 @@ import { PreviewQueryRequest, PreviewQueryResponse, } from "@budibase/types" -import { TestAPI } from "./base" +import { Expectations, TestAPI } from "./base" export class QueryAPI extends TestAPI { create = async (body: Query): Promise => { @@ -14,12 +14,14 @@ export class QueryAPI extends TestAPI { execute = async ( queryId: string, - body?: ExecuteQueryRequest + body?: ExecuteQueryRequest, + expectations?: Expectations ): Promise => { return await this._post( `/api/v2/queries/${queryId}`, { body, + expectations, } ) }