query / update events + tests

This commit is contained in:
Rory Powell 2022-04-07 00:38:18 +01:00
parent 8a08e9322f
commit ac8573b67e
17 changed files with 148 additions and 71 deletions

View File

@ -38,9 +38,6 @@ exports.Events = {
ORG_LOGO_UPDATED: "org:info:logo:updated", ORG_LOGO_UPDATED: "org:info:logo:updated",
ORG_PLATFORM_URL_UPDATED: "org:platformurl:updated", ORG_PLATFORM_URL_UPDATED: "org:platformurl:updated",
// ORG / NPS
NPS_SUBMITTED: "nps:submitted",
// ORG / UPDATE // ORG / UPDATE
UPDATE_VERSION_CHECKED: "version:checked", UPDATE_VERSION_CHECKED: "version:checked",
@ -77,8 +74,8 @@ exports.Events = {
QUERY_CREATED: "query:created", QUERY_CREATED: "query:created",
QUERY_UPDATED: "query:updated", QUERY_UPDATED: "query:updated",
QUERY_DELETED: "query:deleted", QUERY_DELETED: "query:deleted",
QUERY_IMPORTED: "query:imported", QUERY_IMPORT: "query:import",
QUERY_RUN: "query:run", // QUERY_RUN: "query:run",
QUERY_PREVIEWED: "query:previewed", QUERY_PREVIEWED: "query:previewed",
// TABLE // TABLE
@ -101,10 +98,7 @@ exports.Events = {
VIEW_CALCULATION_DELETED: "view:calculation:created", VIEW_CALCULATION_DELETED: "view:calculation:created",
// ROW // ROW
ROW_CREATED: "row:created", // ROW_CREATED: "row:created",
ROW_UPDATED: "row:updated",
ROW_DELETED: "row:deleted",
ROW_IMPORTED: "row:imported",
// BUILDER // BUILDER
BUILDER_SERVED: "builder:served", BUILDER_SERVED: "builder:served",
@ -134,7 +128,7 @@ exports.Events = {
LICENSE_UPGRADED: "license:upgraded", LICENSE_UPGRADED: "license:upgraded",
LICENSE_DOWNGRADED: "license:downgraded", LICENSE_DOWNGRADED: "license:downgraded",
LICENSE_UPDATED: "license:updated", LICENSE_UPDATED: "license:updated",
LICENSE_PAIRED: "license:paired", LICENSE_ACTIVATED: "license:activated",
LICENSE_QUOTA_EXCEEDED: "license:quota:exceeded", LICENSE_QUOTA_EXCEEDED: "license:quota:exceeded",
// ACCOUNT // ACCOUNT

View File

@ -16,9 +16,9 @@ exports.updated = () => {
events.processEvent(Events.LICENSE_UPDATED, properties) events.processEvent(Events.LICENSE_UPDATED, properties)
} }
exports.paired = () => { exports.activated = () => {
const properties = {} const properties = {}
events.processEvent(Events.LICENSE_PAIRED, properties) events.processEvent(Events.LICENSE_ACTIVATED, properties)
} }
exports.quotaExceeded = (quotaName, value) => { exports.quotaExceeded = (quotaName, value) => {

View File

@ -23,12 +23,8 @@ exports.versionChecked = version => {
events.processEvent(Events.UPDATE_VERSION_CHECKED, properties) events.processEvent(Events.UPDATE_VERSION_CHECKED, properties)
} }
// TODO
exports.analyticsOptOut = () => { exports.analyticsOptOut = () => {
const properties = {} const properties = {}
events.processEvent(Events.ANALYTICS_OPT_OUT, properties) events.processEvent(Events.ANALYTICS_OPT_OUT, properties)
} }
exports.npsSubmitted = () => {
const properties = {}
events.processEvent(Events.NPS_SUBMITTED, properties)
}

View File

@ -6,29 +6,27 @@ exports.created = () => {
events.processEvent(Events.QUERY_CREATED, properties) events.processEvent(Events.QUERY_CREATED, properties)
} }
// TODO
exports.updated = () => { exports.updated = () => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_UPDATED, properties) events.processEvent(Events.QUERY_UPDATED, properties)
} }
// TODO
exports.deleted = () => { exports.deleted = () => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_DELETED, properties) events.processEvent(Events.QUERY_DELETED, properties)
} }
// TODO // TODO
exports.imported = () => { exports.import = () => {
const properties = {} const properties = {}
events.processEvent(Events.QUERY_IMPORTED, properties) events.processEvent(Events.QUERY_IMPORT, properties)
} }
// TODO // TODO
exports.run = () => { // exports.run = () => {
const properties = {} // const properties = {}
events.processEvent(Events.QUERY_RUN, properties) // events.processEvent(Events.QUERY_RUN, properties)
} // }
// TODO // TODO
exports.previewed = () => { exports.previewed = () => {

View File

@ -1,26 +1,7 @@
const events = require("../events") // const events = require("../events")
const { Events } = require("../constants") // const { Events } = require("../constants")
exports.created = () => { // exports.created = () => {
const properties = {} // const properties = {}
events.processEvent(Events.ROW_CREATED, properties) // events.processEvent(Events.ROW_CREATED, properties)
} // }
// TODO
exports.imported = () => {
const properties = {}
events.processEvent(Events.ROW_IMPORTED, properties)
exports.rowCreated()
}
// TODO
exports.updated = () => {
const properties = {}
events.processEvent(Events.ROW_UPDATED, properties)
}
// TODO
exports.deleted = () => {
const properties = {}
events.processEvent(Events.ROW_DELETED, properties)
}

View File

@ -54,7 +54,13 @@ jest.mock("../../../events", () => {
platformURLUpdated: jest.fn(), platformURLUpdated: jest.fn(),
versionChecked: jest.fn(), versionChecked: jest.fn(),
analyticsOptOut: jest.fn(), analyticsOptOut: jest.fn(),
npsSubmitted: jest.fn(), },
query: {
created: jest.fn(),
updated: jest.fn(),
deleted: jest.fn(),
import: jest.fn(),
previewed: jest.fn(),
}, },
} }
}) })

View File

@ -14,11 +14,6 @@ exports.endUserPing = async ctx => {
return return
} }
// posthogClient.identify({
// distinctId: ctx.user && ctx.user._id,
// properties: {},
// })
analytics.captureEvent(ctx.user._id, "budibase:end_user_ping", { analytics.captureEvent(ctx.user._id, "budibase:end_user_ping", {
appId: ctx.appId, appId: ctx.appId,
}) })

View File

@ -7,6 +7,8 @@ import { Query } from "./../../../../definitions/common"
import { Curl } from "./sources/curl" import { Curl } from "./sources/curl"
// @ts-ignore // @ts-ignore
import { getAppDB } from "@budibase/backend-core/context" import { getAppDB } from "@budibase/backend-core/context"
import { events } from "@budibase/backend-core"
interface ImportResult { interface ImportResult {
errorQueries: Query[] errorQueries: Query[]
queries: Query[] queries: Query[]
@ -36,7 +38,7 @@ export class RestImporter {
} }
importQueries = async (datasourceId: string): Promise<ImportResult> => { importQueries = async (datasourceId: string): Promise<ImportResult> => {
// constuct the queries // construct the queries
let queries = await this.source.getQueries(datasourceId) let queries = await this.source.getQueries(datasourceId)
// validate queries // validate queries
@ -76,9 +78,20 @@ export class RestImporter {
} }
}) })
const successQueries = Object.values(queryIndex)
// events
const count = successQueries.length
const importSource = this.source.getImportSource()
const datasource = await db.get(datasourceId)
events.query.import({ datasource, importSource, count })
for (let query of successQueries) {
events.query.created(query)
}
return { return {
errorQueries, errorQueries,
queries: Object.values(queryIndex), queries: successQueries,
} }
} }
} }

View File

@ -17,6 +17,7 @@ export abstract class ImportSource {
abstract isSupported(data: string): Promise<boolean> abstract isSupported(data: string): Promise<boolean>
abstract getInfo(): Promise<ImportInfo> abstract getInfo(): Promise<ImportInfo>
abstract getQueries(datasourceId: string): Promise<Query[]> abstract getQueries(datasourceId: string): Promise<Query[]>
abstract getImportSource(): string
constructQuery = ( constructQuery = (
datasourceId: string, datasourceId: string,

View File

@ -71,6 +71,10 @@ export class Curl extends ImportSource {
} }
} }
getImportSource(): string {
return "curl"
}
getQueries = async (datasourceId: string): Promise<Query[]> => { getQueries = async (datasourceId: string): Promise<Query[]> => {
const url = this.getUrl() const url = this.getUrl()
const name = url.pathname const name = url.pathname

View File

@ -70,6 +70,10 @@ export class OpenAPI2 extends OpenAPISource {
} }
} }
getImportSource(): string {
return "openapi2.0"
}
getQueries = async (datasourceId: string): Promise<Query[]> => { getQueries = async (datasourceId: string): Promise<Query[]> => {
const url = this.getUrl() const url = this.getUrl()
const queries = [] const queries = []

View File

@ -106,6 +106,10 @@ export class OpenAPI3 extends OpenAPISource {
} }
} }
getImportSource(): string {
return "openapi3.0"
}
getQueries = async (datasourceId: string): Promise<Query[]> => { getQueries = async (datasourceId: string): Promise<Query[]> => {
let url: string | URL | undefined let url: string | URL | undefined
if (this.document.servers?.length) { if (this.document.servers?.length) {

View File

@ -1,9 +1,10 @@
const TestConfig = require("../../../../../tests/utilities/TestConfiguration") const TestConfig = require("../../../../../tests/utilities/TestConfiguration")
const { RestImporter } = require("../index") const { RestImporter } = require("../index")
const fs = require("fs") const fs = require("fs")
const path = require('path') const path = require('path')
const { events} = require("@budibase/backend-core")
const { mocks } = require("@budibase/backend-core/testUtils")
mocks.date.mock()
const getData = (file) => { const getData = (file) => {
return fs.readFileSync(path.join(__dirname, `../sources/tests/${file}`), "utf8") return fs.readFileSync(path.join(__dirname, `../sources/tests/${file}`), "utf8")
@ -103,9 +104,13 @@ describe("Rest Importer", () => {
const testImportQueries = async (key, data, assertions) => { const testImportQueries = async (key, data, assertions) => {
await init(data) await init(data)
const importResult = await restImporter.importQueries("datasourceId") const datasource = await config.createDatasource()
const importResult = await restImporter.importQueries(datasource._id)
expect(importResult.errorQueries.length).toBe(0) expect(importResult.errorQueries.length).toBe(0)
expect(importResult.queries.length).toBe(assertions[key].count) expect(importResult.queries.length).toBe(assertions[key].count)
expect(events.query.import).toBeCalledTimes(1)
const eventData = { datasource, importSource: assertions[key].source, count: assertions[key].count}
expect(events.query.import).toBeCalledWith(eventData)
jest.clearAllMocks() jest.clearAllMocks()
} }
@ -116,32 +121,41 @@ describe("Rest Importer", () => {
// openapi2 (swagger) // openapi2 (swagger)
"oapi2CrudJson" : { "oapi2CrudJson" : {
count: 6, count: 6,
source: "openapi2.0",
}, },
"oapi2CrudYaml" :{ "oapi2CrudYaml" :{
count: 6, count: 6,
source: "openapi2.0"
}, },
"oapi2PetstoreJson" : { "oapi2PetstoreJson" : {
count: 20, count: 20,
source: "openapi2.0"
}, },
"oapi2PetstoreYaml" :{ "oapi2PetstoreYaml" :{
count: 20, count: 20,
source: "openapi2.0"
}, },
// openapi3 // openapi3
"oapi3CrudJson" : { "oapi3CrudJson" : {
count: 6, count: 6,
source: "openapi3.0"
}, },
"oapi3CrudYaml" :{ "oapi3CrudYaml" :{
count: 6, count: 6,
source: "openapi3.0"
}, },
"oapi3PetstoreJson" : { "oapi3PetstoreJson" : {
count: 19, count: 19,
source: "openapi3.0"
}, },
"oapi3PetstoreYaml" :{ "oapi3PetstoreYaml" :{
count: 19, count: 19,
source: "openapi3.0"
}, },
// curl // curl
"curl": { "curl": {
count: 1 count: 1,
source: "curl"
} }
} }
await runTest(testImportQueries, assertions) await runTest(testImportQueries, assertions)

View File

@ -7,6 +7,7 @@ import { invalidateDynamicVariables } from "../../../threads/utils"
import { QUERY_THREAD_TIMEOUT } from "../../../environment" import { QUERY_THREAD_TIMEOUT } from "../../../environment"
import { getAppDB } from "@budibase/backend-core/context" import { getAppDB } from "@budibase/backend-core/context"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { events } from "@budibase/backend-core"
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: QUERY_THREAD_TIMEOUT || 10000, timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
@ -80,11 +81,18 @@ export async function save(ctx: any) {
const db = getAppDB() const db = getAppDB()
const query = ctx.request.body const query = ctx.request.body
const datasource = await db.get(query.datasourceId)
let eventFn
if (!query._id) { if (!query._id) {
query._id = generateQueryID(query.datasourceId) query._id = generateQueryID(query.datasourceId)
eventFn = () => events.query.created(datasource, query)
} else {
eventFn = () => events.query.updated(datasource, query)
} }
const response = await db.put(query) const response = await db.put(query)
eventFn()
query._rev = response.rev query._rev = response.rev
ctx.body = query ctx.body = query
@ -124,6 +132,7 @@ export async function preview(ctx: any) {
}) })
const { rows, keys, info, extra } = await quotas.addQuery(runFn) const { rows, keys, info, extra } = await quotas.addQuery(runFn)
events.query.previewed(datasource)
ctx.body = { ctx.body = {
rows, rows,
schemaFields: [...new Set(keys)], schemaFields: [...new Set(keys)],
@ -211,4 +220,5 @@ export async function destroy(ctx: any) {
await db.remove(ctx.params.queryId, ctx.params.revId) await db.remove(ctx.params.queryId, ctx.params.revId)
ctx.message = `Query deleted.` ctx.message = `Query deleted.`
ctx.status = 200 ctx.status = 200
events.query.deleted()
} }

View File

@ -1,5 +1,6 @@
const setup = require("./utilities") const setup = require("./utilities")
const { events } = require("@budibase/backend-core") const { events } = require("@budibase/backend-core")
const version = require("../../../../package.json").version
describe("/dev", () => { describe("/dev", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -19,7 +20,21 @@ describe("/dev", () => {
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(events.app.reverted.mock.calls.length).toBe(1) expect(events.app.reverted).toBeCalledTimes(1)
})
})
describe("version", () => {
it("should get the installation version", async () => {
const res = await request
.get(`/api/dev/version`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.version).toBe(version)
expect(events.org.versionChecked).toBeCalledTimes(1)
expect(events.org.versionChecked).toBeCalledWith(version)
}) })
}) })
}) })

View File

@ -15,6 +15,7 @@ const { checkCacheForDynamicVariable } = require("../../../threads/utils")
const { basicQuery, basicDatasource } = setup.structures const { basicQuery, basicDatasource } = setup.structures
const { mocks } = require("@budibase/backend-core/testUtils") const { mocks } = require("@budibase/backend-core/testUtils")
mocks.date.mock() mocks.date.mock()
const { events } = require("@budibase/backend-core")
describe("/queries", () => { describe("/queries", () => {
let request = setup.getRequest() let request = setup.getRequest()
@ -40,16 +41,21 @@ describe("/queries", () => {
return { datasource, query } return { datasource, query }
} }
describe("create", () => { const createQuery = async (query) => {
it("should create a new query", async () => { return request
const { _id } = await config.createDatasource()
const query = basicQuery(_id)
const res = await request
.post(`/api/queries`) .post(`/api/queries`)
.send(query) .send(query)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
}
describe("create", () => {
it("should create a new query", async () => {
const { _id } = await config.createDatasource()
const query = basicQuery(_id)
jest.clearAllMocks()
const res = await createQuery(query)
expect(res.res.statusMessage).toEqual( expect(res.res.statusMessage).toEqual(
`Query ${query.name} saved successfully.` `Query ${query.name} saved successfully.`
@ -61,6 +67,33 @@ describe("/queries", () => {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString()
}) })
expect(events.query.created).toBeCalledTimes(1)
expect(events.query.updated).not.toBeCalled()
})
})
describe("update", () => {
it("should update query", async () => {
const { _id } = await config.createDatasource()
const query = basicQuery(_id)
const res = await createQuery(query)
jest.clearAllMocks()
query._id = res.body._id
query._rev = res.body._rev
await createQuery(query)
expect(res.res.statusMessage).toEqual(
`Query ${query.name} saved successfully.`
)
expect(res.body).toEqual({
_rev: res.body._rev,
_id: res.body._id,
...query,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
})
expect(events.query.created).not.toBeCalled()
expect(events.query.updated).toBeCalledTimes(1)
}) })
}) })
@ -155,6 +188,7 @@ describe("/queries", () => {
.expect(200) .expect(200)
expect(res.body).toEqual([]) expect(res.body).toEqual([])
expect(events.query.deleted).toBeCalledTimes(1)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
@ -183,6 +217,9 @@ describe("/queries", () => {
// these responses come from the mock // these responses come from the mock
expect(res.body.schemaFields).toEqual(["a", "b"]) expect(res.body.schemaFields).toEqual(["a", "b"])
expect(res.body.rows.length).toEqual(1) expect(res.body.rows.length).toEqual(1)
expect(events.query.previewed).toBeCalledTimes(1)
datasource.config = { schema: "public" }
expect(events.query.previewed).toBeCalledWith(datasource)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {

View File

@ -62,7 +62,11 @@ const getEventFns = async (db, config) => {
// platform url // platform url
const platformUrl = config.config.platformUrl const platformUrl = config.config.platformUrl
if (platformUrl && platformUrl !== "http://localhost:10000") { if (
platformUrl &&
platformUrl !== "http://localhost:10000" &&
env.SELF_HOSTED
) {
fns.push(events.org.platformURLUpdated) fns.push(events.org.platformURLUpdated)
} }
break break
@ -119,7 +123,8 @@ const getEventFns = async (db, config) => {
if ( if (
platformUrl && platformUrl &&
platformUrl !== "http://localhost:10000" && platformUrl !== "http://localhost:10000" &&
existingPlatformUrl !== platformUrl existingPlatformUrl !== platformUrl &&
env.SELF_HOSTED
) { ) {
fns.push(events.org.platformURLUpdated) fns.push(events.org.platformURLUpdated)
} }