Merge branch 'master' into fix/BUDI-7444

This commit is contained in:
Michael Drury 2024-03-25 10:03:38 +00:00 committed by GitHub
commit 5240c2c2ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 147 additions and 380 deletions

1
.gitignore vendored
View File

@ -107,3 +107,4 @@ budibase-component
budibase-datasource budibase-datasource
*.iml *.iml
.nx

View File

@ -32,10 +32,14 @@
import TourWrap from "components/portal/onboarding/TourWrap.svelte" import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import PosthogClient from "../../analytics/PosthogClient"
export let application export let application
export let loaded export let loaded
const posthog = new PosthogClient(process.env.POSTHOG_TOKEN)
let unpublishModal let unpublishModal
let updateAppModal let updateAppModal
let revertModal let revertModal
@ -44,6 +48,7 @@
let appActionPopoverOpen = false let appActionPopoverOpen = false
let appActionPopoverAnchor let appActionPopoverAnchor
let publishing = false let publishing = false
let showNpsSurvey = false
let lastOpened let lastOpened
$: filteredApps = $appsStore.apps.filter(app => app.devId === application) $: filteredApps = $appsStore.apps.filter(app => app.devId === application)
@ -98,6 +103,7 @@
type: "success", type: "success",
icon: "GlobeCheck", icon: "GlobeCheck",
}) })
showNpsSurvey = true
await completePublish() await completePublish()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -148,6 +154,10 @@
notifications.error("Error refreshing app") notifications.error("Error refreshing app")
} }
} }
onMount(() => {
posthog.init()
})
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
@ -345,6 +355,10 @@
<RevertModal bind:this={revertModal} /> <RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} /> <VersionModal hideIcon bind:this={versionModal} />
{#if showNpsSurvey}
<div class="nps-survey" />
{/if}
<style> <style>
.app-action-popover-content { .app-action-popover-content {
padding: var(--spacing-xl); padding: var(--spacing-xl);

View File

@ -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
}

View File

@ -3,8 +3,6 @@ import * as setup from "../utilities"
import { databaseTestProviders } from "../../../../integrations/tests/utils" import { databaseTestProviders } from "../../../../integrations/tests/utils"
import { MongoClient, type Collection, BSON } from "mongodb" import { MongoClient, type Collection, BSON } from "mongodb"
jest.unmock("mongodb")
const collection = "test_collection" const collection = "test_collection"
const expectValidId = expect.stringMatching(/^\w{24}$/) const expectValidId = expect.stringMatching(/^\w{24}$/)
@ -36,27 +34,27 @@ describe("/queries", () => {
return await config.api.query.save(combinedQuery) return await config.api.query.save(combinedQuery)
} }
async function withClient( async function withClient<T>(
callback: (client: MongoClient) => Promise<void> callback: (client: MongoClient) => Promise<T>
): Promise<void> { ): Promise<T> {
const ds = await databaseTestProviders.mongodb.datasource() const ds = await databaseTestProviders.mongodb.datasource()
const client = new MongoClient(ds.config!.connectionString) const client = new MongoClient(ds.config!.connectionString)
await client.connect() await client.connect()
try { try {
await callback(client) return await callback(client)
} finally { } finally {
await client.close() await client.close()
} }
} }
async function withCollection( async function withCollection<T>(
callback: (collection: Collection) => Promise<void> callback: (collection: Collection) => Promise<T>
): Promise<void> { ): Promise<T> {
await withClient(async client => { return await withClient(async client => {
const db = client.db( const db = client.db(
(await databaseTestProviders.mongodb.datasource()).config!.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 () => { it("should be able to delete all records", async () => {
const query = await createQuery({ const query = await createQuery({
fields: { 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,
})
})
})
}) })

View File

@ -23,7 +23,7 @@ import {
} from "mongodb" } from "mongodb"
import environment from "../environment" import environment from "../environment"
interface MongoDBConfig { export interface MongoDBConfig {
connectionString: string connectionString: string
db: string db: string
tlsCertificateKeyFile: string tlsCertificateKeyFile: string
@ -348,7 +348,7 @@ const getSchema = () => {
const SCHEMA: Integration = getSchema() const SCHEMA: Integration = getSchema()
class MongoIntegration implements IntegrationBase { export class MongoIntegration implements IntegrationBase {
private config: MongoDBConfig private config: MongoDBConfig
private client: MongoClient private client: MongoClient

View File

@ -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',
})
})
})

View File

@ -5,7 +5,7 @@ import {
PreviewQueryRequest, PreviewQueryRequest,
PreviewQueryResponse, PreviewQueryResponse,
} from "@budibase/types" } from "@budibase/types"
import { TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
export class QueryAPI extends TestAPI { export class QueryAPI extends TestAPI {
save = async (body: Query): Promise<Query> => { save = async (body: Query): Promise<Query> => {
@ -14,12 +14,14 @@ export class QueryAPI extends TestAPI {
execute = async ( execute = async (
queryId: string, queryId: string,
body?: ExecuteQueryRequest body?: ExecuteQueryRequest,
expectations?: Expectations
): Promise<ExecuteQueryResponse> => { ): Promise<ExecuteQueryResponse> => {
return await this._post<ExecuteQueryResponse>( return await this._post<ExecuteQueryResponse>(
`/api/v2/queries/${queryId}`, `/api/v2/queries/${queryId}`,
{ {
body, body,
expectations,
} }
) )
} }