Merge master.

This commit is contained in:
Sam Rose 2024-12-09 17:18:38 +00:00
commit 76b5190454
No known key found for this signature in database
30 changed files with 322 additions and 221 deletions

View File

@ -13,9 +13,7 @@ const EXCLUDED_EVENTS: Event[] = [
Event.ROLE_UPDATED, Event.ROLE_UPDATED,
Event.DATASOURCE_UPDATED, Event.DATASOURCE_UPDATED,
Event.QUERY_UPDATED, Event.QUERY_UPDATED,
Event.TABLE_UPDATED,
Event.VIEW_UPDATED, Event.VIEW_UPDATED,
Event.VIEW_FILTER_UPDATED,
Event.VIEW_CALCULATION_UPDATED, Event.VIEW_CALCULATION_UPDATED,
Event.AUTOMATION_TRIGGER_UPDATED, Event.AUTOMATION_TRIGGER_UPDATED,
Event.USER_GROUP_UPDATED, Event.USER_GROUP_UPDATED,

View File

@ -23,3 +23,4 @@ export { default as plugin } from "./plugin"
export { default as backup } from "./backup" export { default as backup } from "./backup"
export { default as environmentVariable } from "./environmentVariable" export { default as environmentVariable } from "./environmentVariable"
export { default as auditLog } from "./auditLog" export { default as auditLog } from "./auditLog"
export { default as rowAction } from "./rowAction"

View File

@ -0,0 +1,13 @@
import { publishEvent } from "../events"
import { Event, RowActionCreatedEvent } from "@budibase/types"
async function created(
rowAction: RowActionCreatedEvent,
timestamp?: string | number
) {
await publishEvent(Event.ROW_ACTION_CREATED, rowAction, timestamp)
}
export default {
created,
}

View File

@ -1,13 +1,14 @@
import { publishEvent } from "../events" import { publishEvent } from "../events"
import { import {
Event, Event,
TableExportFormat, FieldType,
Table, Table,
TableCreatedEvent, TableCreatedEvent,
TableUpdatedEvent,
TableDeletedEvent, TableDeletedEvent,
TableExportedEvent, TableExportedEvent,
TableExportFormat,
TableImportedEvent, TableImportedEvent,
TableUpdatedEvent,
} from "@budibase/types" } from "@budibase/types"
async function created(table: Table, timestamp?: string | number) { async function created(table: Table, timestamp?: string | number) {
@ -20,14 +21,34 @@ async function created(table: Table, timestamp?: string | number) {
await publishEvent(Event.TABLE_CREATED, properties, timestamp) await publishEvent(Event.TABLE_CREATED, properties, timestamp)
} }
async function updated(table: Table) { async function updated(oldTable: Table, newTable: Table) {
// only publish the event if it has fields we are interested in
let defaultValues, aiColumn
// check that new fields have been added
for (const key in newTable.schema) {
if (!oldTable.schema[key]) {
const newColumn = newTable.schema[key]
if ("default" in newColumn && newColumn.default != null) {
defaultValues = true
}
if (newColumn.type === FieldType.AI) {
aiColumn = newColumn.operation
}
}
}
const properties: TableUpdatedEvent = { const properties: TableUpdatedEvent = {
tableId: table._id as string, tableId: newTable._id as string,
defaultValues,
aiColumn,
audited: { audited: {
name: table.name, name: newTable.name,
}, },
} }
await publishEvent(Event.TABLE_UPDATED, properties) if (defaultValues || aiColumn) {
await publishEvent(Event.TABLE_UPDATED, properties)
}
} }
async function deleted(table: Table) { async function deleted(table: Table) {

View File

@ -1,6 +1,11 @@
import { publishEvent } from "../events" import { publishEvent } from "../events"
import { import {
CalculationType,
Event, Event,
Table,
TableExportFormat,
View,
ViewCalculation,
ViewCalculationCreatedEvent, ViewCalculationCreatedEvent,
ViewCalculationDeletedEvent, ViewCalculationDeletedEvent,
ViewCalculationUpdatedEvent, ViewCalculationUpdatedEvent,
@ -11,20 +16,20 @@ import {
ViewFilterDeletedEvent, ViewFilterDeletedEvent,
ViewFilterUpdatedEvent, ViewFilterUpdatedEvent,
ViewUpdatedEvent, ViewUpdatedEvent,
View, ViewV2,
ViewCalculation, ViewJoinCreatedEvent,
Table,
TableExportFormat,
} from "@budibase/types" } from "@budibase/types"
async function created(view: View, timestamp?: string | number) { async function created(view: ViewV2, timestamp?: string | number) {
const properties: ViewCreatedEvent = { const properties: ViewCreatedEvent = {
name: view.name,
type: view.type,
tableId: view.tableId, tableId: view.tableId,
} }
await publishEvent(Event.VIEW_CREATED, properties, timestamp) await publishEvent(Event.VIEW_CREATED, properties, timestamp)
} }
async function updated(view: View) { async function updated(view: ViewV2) {
const properties: ViewUpdatedEvent = { const properties: ViewUpdatedEvent = {
tableId: view.tableId, tableId: view.tableId,
} }
@ -46,16 +51,27 @@ async function exported(table: Table, format: TableExportFormat) {
await publishEvent(Event.VIEW_EXPORTED, properties) await publishEvent(Event.VIEW_EXPORTED, properties)
} }
async function filterCreated(view: View, timestamp?: string | number) { async function filterCreated(
{ tableId, filterGroups }: { tableId: string; filterGroups: number },
timestamp?: string | number
) {
const properties: ViewFilterCreatedEvent = { const properties: ViewFilterCreatedEvent = {
tableId: view.tableId, tableId,
filterGroups,
} }
await publishEvent(Event.VIEW_FILTER_CREATED, properties, timestamp) await publishEvent(Event.VIEW_FILTER_CREATED, properties, timestamp)
} }
async function filterUpdated(view: View) { async function filterUpdated({
tableId,
filterGroups,
}: {
tableId: string
filterGroups: number
}) {
const properties: ViewFilterUpdatedEvent = { const properties: ViewFilterUpdatedEvent = {
tableId: view.tableId, tableId: tableId,
filterGroups,
} }
await publishEvent(Event.VIEW_FILTER_UPDATED, properties) await publishEvent(Event.VIEW_FILTER_UPDATED, properties)
} }
@ -67,10 +83,16 @@ async function filterDeleted(view: View) {
await publishEvent(Event.VIEW_FILTER_DELETED, properties) await publishEvent(Event.VIEW_FILTER_DELETED, properties)
} }
async function calculationCreated(view: View, timestamp?: string | number) { async function calculationCreated(
{
tableId,
calculationType,
}: { tableId: string; calculationType: CalculationType },
timestamp?: string | number
) {
const properties: ViewCalculationCreatedEvent = { const properties: ViewCalculationCreatedEvent = {
tableId: view.tableId, tableId,
calculation: view.calculation as ViewCalculation, calculation: calculationType,
} }
await publishEvent(Event.VIEW_CALCULATION_CREATED, properties, timestamp) await publishEvent(Event.VIEW_CALCULATION_CREATED, properties, timestamp)
} }
@ -91,6 +113,13 @@ async function calculationDeleted(existingView: View) {
await publishEvent(Event.VIEW_CALCULATION_DELETED, properties) await publishEvent(Event.VIEW_CALCULATION_DELETED, properties)
} }
async function viewJoinCreated(tableId: any, timestamp?: string | number) {
const properties: ViewJoinCreatedEvent = {
tableId,
}
await publishEvent(Event.VIEW_JOIN_CREATED, properties, timestamp)
}
export default { export default {
created, created,
updated, updated,
@ -102,4 +131,5 @@ export default {
calculationCreated, calculationCreated,
calculationUpdated, calculationUpdated,
calculationDeleted, calculationDeleted,
viewJoinCreated,
} }

View File

@ -117,6 +117,7 @@ beforeAll(async () => {
jest.spyOn(events.view, "calculationCreated") jest.spyOn(events.view, "calculationCreated")
jest.spyOn(events.view, "calculationUpdated") jest.spyOn(events.view, "calculationUpdated")
jest.spyOn(events.view, "calculationDeleted") jest.spyOn(events.view, "calculationDeleted")
jest.spyOn(events.view, "viewJoinCreated")
jest.spyOn(events.plugin, "init") jest.spyOn(events.plugin, "init")
jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "imported")

View File

@ -46,7 +46,7 @@
} }
} else { } else {
// Leave the core data as it is // Leave the core data as it is
return testData return cloneDeep(testData)
} }
} }
@ -63,7 +63,10 @@
return true return true
} }
$: testData = testData || parseTestData($selectedAutomation.data.testData) $: currentTestData = $selectedAutomation.data.testData
// Can be updated locally to avoid race condition when testing
$: testData = parseTestData(currentTestData)
$: { $: {
// clone the trigger so we're not mutating the reference // clone the trigger so we're not mutating the reference
@ -85,7 +88,7 @@
required => testData?.[required] || required !== "row" required => testData?.[required] || required !== "row"
) )
function parseTestJSON(e) { async function parseTestJSON(e) {
let jsonUpdate let jsonUpdate
try { try {
@ -105,7 +108,9 @@
} }
} }
automationStore.actions.addTestDataToAutomation(jsonUpdate) const updatedAuto =
automationStore.actions.addTestDataToAutomation(jsonUpdate)
await automationStore.actions.save(updatedAuto)
} }
const testAutomation = async () => { const testAutomation = async () => {
@ -150,10 +155,14 @@
{#if selectedValues} {#if selectedValues}
<div class="tab-content-padding"> <div class="tab-content-padding">
<AutomationBlockSetup <AutomationBlockSetup
bind:testData
{schemaProperties} {schemaProperties}
isTestModal isTestModal
{testData}
block={trigger} block={trigger}
on:update={e => {
const { testData: updatedTestData } = e.detail
testData = updatedTestData
}}
/> />
</div> </div>
{/if} {/if}
@ -162,7 +171,7 @@
<TextArea <TextArea
value={JSON.stringify($selectedAutomation.data.testData, null, 2)} value={JSON.stringify($selectedAutomation.data.testData, null, 2)}
error={failedParse} error={failedParse}
on:change={e => parseTestJSON(e)} on:change={async e => await parseTestJSON(e)}
/> />
</div> </div>
{/if} {/if}

View File

@ -48,7 +48,7 @@
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core" import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
import { getSchemaForDatasourcePlus } from "dataBinding" import { getSchemaForDatasourcePlus } from "dataBinding"
import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { import {
@ -67,6 +67,8 @@
export let isTestModal = false export let isTestModal = false
export let bindings = [] export let bindings = []
const dispatch = createEventDispatcher()
// Stop unnecessary rendering // Stop unnecessary rendering
const memoBlock = memo(block) const memoBlock = memo(block)
@ -503,15 +505,7 @@
row: { "Active": true, "Order Id" : 14, ... } row: { "Active": true, "Order Id" : 14, ... }
}) })
*/ */
const onChange = async update => { const onChange = Utils.sequential(async update => {
if (isTestModal) {
testData = update
}
updateAutomation(update)
}
const updateAutomation = Utils.sequential(async update => {
const request = cloneDeep(update) const request = cloneDeep(update)
// Process app trigger updates // Process app trigger updates
if (isTrigger && !isTestModal) { if (isTrigger && !isTestModal) {
@ -540,7 +534,9 @@
} }
try { try {
if (isTestModal) { if (isTestModal) {
let newTestData = { schema } // Be sure to merge in the testData prop data, as it can contain custom
// default data
let newTestData = { schema, ...testData }
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents // Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === TriggerStepID.WEBHOOK) { if (stepId === TriggerStepID.WEBHOOK) {
@ -557,7 +553,13 @@
...request, ...request,
} }
await automationStore.actions.addTestDataToAutomation(newTestData) const updatedAuto =
automationStore.actions.addTestDataToAutomation(newTestData)
// Ensure the test request has the latest info.
dispatch("update", updatedAuto)
await automationStore.actions.save(updatedAuto)
} else { } else {
const data = { schema, ...request } const data = { schema, ...request }
await automationStore.actions.updateBlockInputs(block, data) await automationStore.actions.updateBlockInputs(block, data)

View File

@ -880,13 +880,13 @@ const automationActions = store => ({
appId, appId,
}) })
}, },
addTestDataToAutomation: async data => { addTestDataToAutomation: data => {
let newAutomation = cloneDeep(get(selectedAutomation).data) let newAutomation = cloneDeep(get(selectedAutomation).data)
newAutomation.testData = { newAutomation.testData = {
...newAutomation.testData, ...newAutomation.testData,
...data, ...data,
} }
await store.actions.save(newAutomation) return newAutomation
}, },
constructBlock(type, stepId, blockDefinition) { constructBlock(type, stepId, blockDefinition) {
let newName let newName

View File

@ -6,6 +6,7 @@ import {
RowActionResponse, RowActionResponse,
RowActionsResponse, RowActionsResponse,
} from "@budibase/types" } from "@budibase/types"
import { events } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
async function getTable(ctx: Ctx) { async function getTable(ctx: Ctx) {
@ -59,6 +60,8 @@ export async function create(
name: ctx.request.body.name, name: ctx.request.body.name,
}) })
await events.rowAction.created(createdAction)
ctx.body = { ctx.body = {
tableId, tableId,
id: createdAction.id, id: createdAction.id,

View File

@ -45,13 +45,13 @@ export async function updateTable(
inputs.created = true inputs.created = true
} }
try { try {
const { datasource, table } = await sdk.tables.external.save( const { datasource, oldTable, table } = await sdk.tables.external.save(
datasourceId!, datasourceId!,
inputs, inputs,
{ tableId, renaming } { tableId, renaming }
) )
builderSocket?.emitDatasourceUpdate(ctx, datasource) builderSocket?.emitDatasourceUpdate(ctx, datasource)
return table return { table, oldTable }
} catch (err: any) { } catch (err: any) {
if (err instanceof Error) { if (err instanceof Error) {
ctx.throw(400, err.message) ctx.throw(400, err.message)

View File

@ -120,8 +120,15 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
await events.table.created(savedTable) await events.table.created(savedTable)
} else { } else {
const api = pickApi({ table }) const api = pickApi({ table })
savedTable = await api.updateTable(ctx, renaming) const { table: updatedTable, oldTable } = await api.updateTable(
await events.table.updated(savedTable) ctx,
renaming
)
savedTable = updatedTable
if (oldTable) {
await events.table.updated(oldTable, savedTable)
}
} }
if (renaming) { if (renaming) {
await sdk.views.renameLinkedViews(savedTable, renaming) await sdk.views.renameLinkedViews(savedTable, renaming)

View File

@ -30,14 +30,14 @@ export async function updateTable(
} }
try { try {
const { table } = await sdk.tables.internal.save(tableToSave, { const { table, oldTable } = await sdk.tables.internal.save(tableToSave, {
userId: ctx.user._id, userId: ctx.user._id,
rowsToImport: rows, rowsToImport: rows,
tableId: ctx.request.body._id, tableId: ctx.request.body._id,
renaming, renaming,
}) })
return table return { table, oldTable }
} catch (err: any) { } catch (err: any) {
if (err instanceof Error) { if (err instanceof Error) {
ctx.throw(400, err.message) ctx.throw(400, err.message)

View File

@ -19,8 +19,6 @@ import { builderSocket } from "../../../websockets"
const cloneDeep = require("lodash/cloneDeep") const cloneDeep = require("lodash/cloneDeep")
import isEqual from "lodash/isEqual"
export async function fetch(ctx: Ctx) { export async function fetch(ctx: Ctx) {
ctx.body = await getViews() ctx.body = await getViews()
} }
@ -60,71 +58,11 @@ export async function save(ctx: Ctx) {
existingTable.views[viewName] = existingTable.views[originalName] existingTable.views[viewName] = existingTable.views[originalName]
} }
await db.put(table) await db.put(table)
await handleViewEvents(
existingTable.views[viewName] as View,
table.views[viewName]
)
ctx.body = table.views[viewName] ctx.body = table.views[viewName]
builderSocket?.emitTableUpdate(ctx, table) builderSocket?.emitTableUpdate(ctx, table)
} }
export async function calculationEvents(existingView: View, newView: View) {
const existingCalculation = existingView && existingView.calculation
const newCalculation = newView && newView.calculation
if (existingCalculation && !newCalculation) {
await events.view.calculationDeleted(existingView)
}
if (!existingCalculation && newCalculation) {
await events.view.calculationCreated(newView)
}
if (
existingCalculation &&
newCalculation &&
existingCalculation !== newCalculation
) {
await events.view.calculationUpdated(newView)
}
}
export async function filterEvents(existingView: View, newView: View) {
const hasExistingFilters = !!(
existingView &&
existingView.filters &&
existingView.filters.length
)
const hasNewFilters = !!(newView && newView.filters && newView.filters.length)
if (hasExistingFilters && !hasNewFilters) {
await events.view.filterDeleted(newView)
}
if (!hasExistingFilters && hasNewFilters) {
await events.view.filterCreated(newView)
}
if (
hasExistingFilters &&
hasNewFilters &&
!isEqual(existingView.filters, newView.filters)
) {
await events.view.filterUpdated(newView)
}
}
async function handleViewEvents(existingView: View, newView: View) {
if (!existingView) {
await events.view.created(newView)
} else {
await events.view.updated(newView)
}
await calculationEvents(existingView, newView)
await filterEvents(existingView, newView)
}
export async function destroy(ctx: Ctx) { export async function destroy(ctx: Ctx) {
const db = context.getAppDB() const db = context.getAppDB()
const viewName = decodeURIComponent(ctx.params.viewName) const viewName = decodeURIComponent(ctx.params.viewName)

View File

@ -17,6 +17,7 @@ import {
CreateViewResponse, CreateViewResponse,
UpdateViewResponse, UpdateViewResponse,
} from "@budibase/types" } from "@budibase/types"
import { events } from "@budibase/backend-core"
import { builderSocket, gridSocket } from "../../../websockets" import { builderSocket, gridSocket } from "../../../websockets"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
@ -150,6 +151,9 @@ export async function create(ctx: Ctx<CreateViewRequest, CreateViewResponse>) {
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
} }
const result = await sdk.views.create(tableId, parsedView) const result = await sdk.views.create(tableId, parsedView)
await events.view.created(result)
ctx.status = 201 ctx.status = 201
ctx.body = { ctx.body = {
data: result, data: result,
@ -160,6 +164,46 @@ export async function create(ctx: Ctx<CreateViewRequest, CreateViewResponse>) {
gridSocket?.emitViewUpdate(ctx, result) gridSocket?.emitViewUpdate(ctx, result)
} }
async function handleViewFilterEvents(existingView: ViewV2, view: ViewV2) {
const filterGroups = view.queryUI?.groups?.length || 0
const properties = { filterGroups, tableId: view.tableId }
if (
filterGroups >= 2 &&
filterGroups > (existingView?.queryUI?.groups?.length || 0)
) {
await events.view.filterUpdated(properties)
}
}
async function handleViewEvents(existingView: ViewV2, view: ViewV2) {
// Grouped filters
if (view.queryUI?.groups) {
await handleViewFilterEvents(existingView, view)
}
// if new columns in the view
for (const key in view.schema) {
if ("calculationType" in view.schema[key] && !existingView?.schema?.[key]) {
await events.view.calculationCreated({
calculationType: view.schema[key].calculationType,
tableId: view.tableId,
})
}
// view joins
for (const column in view.schema[key]?.columns ?? []) {
// if the new column is visible and it wasn't before
if (
!existingView?.schema?.[key].columns?.[column].visible &&
view.schema?.[key].columns?.[column].visible
) {
// new view join exposing a column
await events.view.viewJoinCreated({ tableId: view.tableId })
}
}
}
}
export async function update(ctx: Ctx<UpdateViewRequest, UpdateViewResponse>) { export async function update(ctx: Ctx<UpdateViewRequest, UpdateViewResponse>) {
const view = ctx.request.body const view = ctx.request.body
@ -187,10 +231,15 @@ export async function update(ctx: Ctx<UpdateViewRequest, UpdateViewResponse>) {
primaryDisplay: view.primaryDisplay, primaryDisplay: view.primaryDisplay,
} }
const result = await sdk.views.update(tableId, parsedView) const { view: result, existingView } = await sdk.views.update(
ctx.body = { tableId,
data: result, parsedView
} )
await handleViewEvents(existingView, result)
await events.view.updated(result)
ctx.body = { data: result }
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
builderSocket?.emitTableUpdate(ctx, table) builderSocket?.emitTableUpdate(ctx, table)

View File

@ -247,6 +247,9 @@ if (descriptions.length) {
}, },
}, },
}, },
primary: ["_id"],
views: {},
sql: true,
}) })
) )
@ -254,9 +257,8 @@ if (descriptions.length) {
...table, ...table,
name: generator.guid(), name: generator.guid(),
}) })
expect(events.table.updated).toHaveBeenCalledTimes(1) expect(events.table.updated).toHaveBeenCalledTimes(1)
expect(events.table.updated).toHaveBeenCalledWith(updatedTable) expect(events.table.updated).toHaveBeenCalledWith(table, updatedTable)
}) })
it("updates all the row fields for a table when a schema key is renamed", async () => { it("updates all the row fields for a table when a schema key is renamed", async () => {

View File

@ -73,25 +73,12 @@ describe("/views", () => {
} }
describe("create", () => { describe("create", () => {
it("returns a success message when the view is successfully created", async () => {
await saveView()
expect(events.view.created).toHaveBeenCalledTimes(1)
})
it("creates a view with a calculation", async () => { it("creates a view with a calculation", async () => {
jest.clearAllMocks() jest.clearAllMocks()
const view = await saveView({ calculation: ViewCalculation.COUNT }) const view = await saveView({ calculation: ViewCalculation.COUNT })
expect(view.tableId).toBe(table._id) expect(view.tableId).toBe(table._id)
expect(events.view.created).toHaveBeenCalledTimes(1)
expect(events.view.updated).not.toHaveBeenCalled()
expect(events.view.calculationCreated).toHaveBeenCalledTimes(1)
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("creates a view with a filter", async () => { it("creates a view with a filter", async () => {
@ -109,14 +96,6 @@ describe("/views", () => {
}) })
expect(view.tableId).toBe(table._id) expect(view.tableId).toBe(table._id)
expect(events.view.created).toHaveBeenCalledTimes(1)
expect(events.view.updated).not.toHaveBeenCalled()
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).toHaveBeenCalledTimes(1)
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("updates the table row with the new view metadata", async () => { it("updates the table row with the new view metadata", async () => {
@ -166,13 +145,6 @@ describe("/views", () => {
await saveView() await saveView()
expect(events.view.created).not.toHaveBeenCalled() expect(events.view.created).not.toHaveBeenCalled()
expect(events.view.updated).toHaveBeenCalledTimes(1)
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("updates a view calculation", async () => { it("updates a view calculation", async () => {
@ -182,13 +154,6 @@ describe("/views", () => {
await saveView({ calculation: ViewCalculation.COUNT }) await saveView({ calculation: ViewCalculation.COUNT })
expect(events.view.created).not.toHaveBeenCalled() expect(events.view.created).not.toHaveBeenCalled()
expect(events.view.updated).toHaveBeenCalledTimes(1)
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).toHaveBeenCalledTimes(1)
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("deletes a view calculation", async () => { it("deletes a view calculation", async () => {
@ -198,13 +163,6 @@ describe("/views", () => {
await saveView({ calculation: undefined }) await saveView({ calculation: undefined })
expect(events.view.created).not.toHaveBeenCalled() expect(events.view.created).not.toHaveBeenCalled()
expect(events.view.updated).toHaveBeenCalledTimes(1)
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).toHaveBeenCalledTimes(1)
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("updates a view filter", async () => { it("updates a view filter", async () => {
@ -230,13 +188,6 @@ describe("/views", () => {
}) })
expect(events.view.created).not.toHaveBeenCalled() expect(events.view.created).not.toHaveBeenCalled()
expect(events.view.updated).toHaveBeenCalledTimes(1)
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).toHaveBeenCalledTimes(1)
expect(events.view.filterDeleted).not.toHaveBeenCalled()
}) })
it("deletes a view filter", async () => { it("deletes a view filter", async () => {
@ -254,13 +205,6 @@ describe("/views", () => {
await saveView({ filters: [] }) await saveView({ filters: [] })
expect(events.view.created).not.toHaveBeenCalled() expect(events.view.created).not.toHaveBeenCalled()
expect(events.view.updated).toHaveBeenCalledTimes(1)
expect(events.view.calculationCreated).not.toHaveBeenCalled()
expect(events.view.calculationUpdated).not.toHaveBeenCalled()
expect(events.view.calculationDeleted).not.toHaveBeenCalled()
expect(events.view.filterCreated).not.toHaveBeenCalled()
expect(events.view.filterUpdated).not.toHaveBeenCalled()
expect(events.view.filterDeleted).toHaveBeenCalledTimes(1)
}) })
}) })

View File

@ -1,39 +1,39 @@
import { import {
ArrayOperator,
BasicOperator,
BBReferenceFieldSubType,
CalculationType,
CreateViewRequest, CreateViewRequest,
Datasource, Datasource,
EmptyFilterOption,
FieldSchema, FieldSchema,
FieldType, FieldType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
JsonFieldSubType,
JsonTypes,
LegacyFilter,
NumericCalculationFieldMetadata,
PermissionLevel, PermissionLevel,
QuotaUsageType, QuotaUsageType,
RelationshipType,
RenameColumn,
Row, Row,
SaveTableRequest, SaveTableRequest,
SearchFilters,
SearchResponse,
SearchViewRowRequest,
SortOrder, SortOrder,
SortType, SortType,
StaticQuotaName, StaticQuotaName,
Table, Table,
TableSchema,
TableSourceType, TableSourceType,
UILogicalOperator,
UISearchFilter,
UpdateViewRequest, UpdateViewRequest,
ViewV2, ViewV2,
SearchResponse,
BasicOperator,
CalculationType,
RelationshipType,
TableSchema,
RenameColumn,
BBReferenceFieldSubType,
NumericCalculationFieldMetadata,
ViewV2Schema, ViewV2Schema,
ViewV2Type, ViewV2Type,
JsonTypes,
EmptyFilterOption,
JsonFieldSubType,
UISearchFilter,
LegacyFilter,
SearchViewRowRequest,
ArrayOperator,
UILogicalOperator,
SearchFilters,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import { import {
@ -42,7 +42,7 @@ import {
} from "../../../integrations/tests/utils" } from "../../../integrations/tests/utils"
import merge from "lodash/merge" import merge from "lodash/merge"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { db, roles, context } from "@budibase/backend-core" import { context, db, events, roles } from "@budibase/backend-core"
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
@ -129,6 +129,7 @@ if (descriptions.length) {
id: expect.stringMatching(new RegExp(`${table._id!}_`)), id: expect.stringMatching(new RegExp(`${table._id!}_`)),
version: 2, version: 2,
}) })
expect(events.view.created).toHaveBeenCalledTimes(1)
}) })
it("can persist views with all fields", async () => { it("can persist views with all fields", async () => {
@ -195,6 +196,7 @@ if (descriptions.length) {
} }
expect(res).toEqual(expected) expect(res).toEqual(expected)
expect(events.view.created).toHaveBeenCalledTimes(1)
}) })
it("can create a view with just a query field, no queryUI, for backwards compatibility", async () => { it("can create a view with just a query field, no queryUI, for backwards compatibility", async () => {
@ -224,6 +226,7 @@ if (descriptions.length) {
}, },
} }
const res = await config.api.viewV2.create(newView) const res = await config.api.viewV2.create(newView)
expect(events.view.created).toHaveBeenCalledTimes(1)
const expected: ViewV2 = { const expected: ViewV2 = {
...newView, ...newView,
@ -283,6 +286,7 @@ if (descriptions.length) {
} }
const createdView = await config.api.viewV2.create(newView) const createdView = await config.api.viewV2.create(newView)
expect(events.view.created).toHaveBeenCalledTimes(1)
expect(createdView).toEqual({ expect(createdView).toEqual({
...newView, ...newView,
@ -990,6 +994,46 @@ if (descriptions.length) {
expect((await config.api.table.get(tableId)).views).toEqual({ expect((await config.api.table.get(tableId)).views).toEqual({
[view.name]: expected, [view.name]: expected,
}) })
expect(events.view.updated).toHaveBeenCalledTimes(1)
})
it("handles view grouped filter events", async () => {
view.queryUI = {
logicalOperator: UILogicalOperator.ALL,
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
groups: [
{
logicalOperator: UILogicalOperator.ALL,
filters: [
{
operator: BasicOperator.EQUAL,
field: "newField",
value: "newValue",
},
],
},
],
}
await config.api.viewV2.update(view)
expect(events.view.filterUpdated).not.toHaveBeenCalled()
// @ts-ignore
view.queryUI.groups.push({
logicalOperator: UILogicalOperator.ALL,
filters: [
{
operator: BasicOperator.EQUAL,
field: "otherField",
value: "otherValue",
},
],
})
await config.api.viewV2.update(view)
expect(events.view.filterUpdated).toHaveBeenCalledWith({
filterGroups: 2,
tableId: view.tableId,
})
}) })
it("can update all fields", async () => { it("can update all fields", async () => {
@ -1621,6 +1665,7 @@ if (descriptions.length) {
field: "age", field: "age",
} }
await config.api.viewV2.update(view) await config.api.viewV2.update(view)
expect(events.view.calculationCreated).toHaveBeenCalledTimes(1)
const { rows } = await config.api.row.search(view.id) const { rows } = await config.api.row.search(view.id)
expect(rows).toHaveLength(2) expect(rows).toHaveLength(2)
@ -2154,6 +2199,7 @@ if (descriptions.length) {
}), }),
}) })
) )
expect(events.view.viewJoinCreated).not.toHaveBeenCalled()
}) })
it("does not rename columns with the same name but from other tables", async () => { it("does not rename columns with the same name but from other tables", async () => {
@ -2226,6 +2272,36 @@ if (descriptions.length) {
) )
}) })
it("handles events for changing column visibility from default false", async () => {
let auxTable = await createAuxTable()
let aux2Table = await createAuxTable()
const table = await createMainTable([
{ name: "aux", tableId: auxTable._id!, fk: "fk_aux" },
{ name: "aux2", tableId: aux2Table._id!, fk: "fk_aux2" },
])
const view = await createView(table._id!, {
aux: {
visible: true,
columns: {
name: { visible: false, readonly: true },
},
},
aux2: {
visible: true,
columns: {
name: { visible: false, readonly: true },
},
},
})
// @ts-expect-error column exists above
view.schema.aux2.columns.name.visible = true
await config.api.viewV2.update(view)
expect(events.view.viewJoinCreated).toHaveBeenCalledTimes(1)
})
it("updates all views references", async () => { it("updates all views references", async () => {
let auxTable = await createAuxTable() let auxTable = await createAuxTable()

View File

@ -7,24 +7,6 @@ export const backfill = async (appDb: Database, timestamp: string | number) => {
for (const table of tables) { for (const table of tables) {
await events.table.created(table, timestamp) await events.table.created(table, timestamp)
if (table.views) {
for (const view of Object.values(table.views)) {
if (sdk.views.isV2(view)) {
continue
}
await events.view.created(view, timestamp)
if (view.calculation) {
await events.view.calculationCreated(view, timestamp)
}
if (view.filters?.length) {
await events.view.filterCreated(view, timestamp)
}
}
}
} }
return tables.length return tables.length

View File

@ -73,16 +73,12 @@ describe("migrations", () => {
expect(events.query.created).toHaveBeenCalledTimes(2) expect(events.query.created).toHaveBeenCalledTimes(2)
expect(events.role.created).toHaveBeenCalledTimes(3) // created roles + admin (created on table creation) expect(events.role.created).toHaveBeenCalledTimes(3) // created roles + admin (created on table creation)
expect(events.table.created).toHaveBeenCalledTimes(3) expect(events.table.created).toHaveBeenCalledTimes(3)
expect(events.view.created).toHaveBeenCalledTimes(2)
expect(events.view.calculationCreated).toHaveBeenCalledTimes(1)
expect(events.view.filterCreated).toHaveBeenCalledTimes(1)
expect(events.screen.created).toHaveBeenCalledTimes(2)
expect(events.backfill.appSucceeded).toHaveBeenCalledTimes(2) expect(events.backfill.appSucceeded).toHaveBeenCalledTimes(2)
// to make sure caching is working as expected // to make sure caching is working as expected
expect( expect(
events.processors.analyticsProcessor.processEvent events.processors.analyticsProcessor.processEvent
).toHaveBeenCalledTimes(24) // Addtion of of the events above ).toHaveBeenCalledTimes(20) // Addition of of the events above
}) })
}) })
}) })

View File

@ -281,7 +281,7 @@ export async function save(
tableToSave.sql = true tableToSave.sql = true
} }
return { datasource: updatedDatasource, table: tableToSave } return { datasource: updatedDatasource, table: tableToSave, oldTable }
} }
export async function destroy(datasourceId: string, table: Table) { export async function destroy(datasourceId: string, table: Table) {

View File

@ -171,7 +171,7 @@ export async function save(
} }
// has to run after, make sure it has _id // has to run after, make sure it has _id
await runStaticFormulaChecks(table, { oldTable, deletion: false }) await runStaticFormulaChecks(table, { oldTable, deletion: false })
return { table } return { table, oldTable }
} }
export async function destroy(table: Table) { export async function destroy(table: Table) {

View File

@ -63,7 +63,7 @@ export async function create(
export async function update( export async function update(
tableId: string, tableId: string,
view: Readonly<ViewV2> view: Readonly<ViewV2>
): Promise<ViewV2> { ): Promise<{ view: Readonly<ViewV2>; existingView: ViewV2 }> {
const db = context.getAppDB() const db = context.getAppDB()
const { datasourceId, tableName } = breakExternalTableId(tableId) const { datasourceId, tableName } = breakExternalTableId(tableId)
@ -87,7 +87,7 @@ export async function update(
delete views[existingView.name] delete views[existingView.name]
views[view.name] = view views[view.name] = view
await db.put(ds) await db.put(ds)
return view return { view, existingView } as { view: ViewV2; existingView: ViewV2 }
} }
export async function remove(viewId: string): Promise<ViewV2> { export async function remove(viewId: string): Promise<ViewV2> {

View File

@ -315,7 +315,10 @@ export async function create(
return view return view
} }
export async function update(tableId: string, view: ViewV2): Promise<ViewV2> { export async function update(
tableId: string,
view: ViewV2
): Promise<{ view: ViewV2; existingView: ViewV2 }> {
await guardViewSchema(tableId, view) await guardViewSchema(tableId, view)
return pickApi(tableId).update(tableId, view) return pickApi(tableId).update(tableId, view)

View File

@ -54,7 +54,7 @@ export async function create(
export async function update( export async function update(
tableId: string, tableId: string,
view: Readonly<ViewV2> view: Readonly<ViewV2>
): Promise<ViewV2> { ): Promise<{ view: ViewV2; existingView: ViewV2 }> {
const db = context.getAppDB() const db = context.getAppDB()
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
table.views ??= {} table.views ??= {}
@ -76,7 +76,7 @@ export async function update(
delete table.views[existingView.name] delete table.views[existingView.name]
table.views[view.name] = view table.views[view.name] = view
await db.put(table) await db.put(table)
return view return { view, existingView } as { view: ViewV2; existingView: ViewV2 }
} }
export async function remove(viewId: string): Promise<ViewV2> { export async function remove(viewId: string): Promise<ViewV2> {

View File

@ -118,6 +118,7 @@ export enum Event {
VIEW_CALCULATION_CREATED = "view:calculation:created", VIEW_CALCULATION_CREATED = "view:calculation:created",
VIEW_CALCULATION_UPDATED = "view:calculation:updated", VIEW_CALCULATION_UPDATED = "view:calculation:updated",
VIEW_CALCULATION_DELETED = "view:calculation:deleted", VIEW_CALCULATION_DELETED = "view:calculation:deleted",
VIEW_JOIN_CREATED = "view:join:created",
// ROWS // ROWS
ROWS_CREATED = "rows:created", ROWS_CREATED = "rows:created",
@ -192,6 +193,9 @@ export enum Event {
// AUDIT LOG // AUDIT LOG
AUDIT_LOGS_FILTERED = "audit_log:filtered", AUDIT_LOGS_FILTERED = "audit_log:filtered",
AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded", AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded",
// ROW ACTION
ROW_ACTION_CREATED = "row_action:created",
} }
export const UserGroupSyncEvents: Event[] = [ export const UserGroupSyncEvents: Event[] = [
@ -376,6 +380,7 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
[Event.VIEW_CALCULATION_CREATED]: undefined, [Event.VIEW_CALCULATION_CREATED]: undefined,
[Event.VIEW_CALCULATION_UPDATED]: undefined, [Event.VIEW_CALCULATION_UPDATED]: undefined,
[Event.VIEW_CALCULATION_DELETED]: undefined, [Event.VIEW_CALCULATION_DELETED]: undefined,
[Event.VIEW_JOIN_CREATED]: undefined,
// SERVED - NOT AUDITED // SERVED - NOT AUDITED
[Event.SERVED_BUILDER]: undefined, [Event.SERVED_BUILDER]: undefined,
@ -395,6 +400,9 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
// AUDIT LOG - NOT AUDITED // AUDIT LOG - NOT AUDITED
[Event.AUDIT_LOGS_FILTERED]: undefined, [Event.AUDIT_LOGS_FILTERED]: undefined,
[Event.AUDIT_LOGS_DOWNLOADED]: undefined, [Event.AUDIT_LOGS_DOWNLOADED]: undefined,
// ROW ACTIONS - NOT AUDITED
[Event.ROW_ACTION_CREATED]: undefined,
} }
// properties added at the final stage of the event pipeline // properties added at the final stage of the event pipeline

View File

@ -24,3 +24,4 @@ export * from "./plugin"
export * from "./backup" export * from "./backup"
export * from "./environmentVariable" export * from "./environmentVariable"
export * from "./auditLog" export * from "./auditLog"
export * from "./rowAction"

View File

@ -0,0 +1,6 @@
import { BaseEvent } from "./event"
export interface RowActionCreatedEvent extends BaseEvent {
name: string
automationId: string
}

View File

@ -1,4 +1,5 @@
import { BaseEvent, TableExportFormat } from "./event" import { BaseEvent, TableExportFormat } from "./event"
import { AIOperationEnum } from "../ai"
export interface TableCreatedEvent extends BaseEvent { export interface TableCreatedEvent extends BaseEvent {
tableId: string tableId: string
@ -9,6 +10,8 @@ export interface TableCreatedEvent extends BaseEvent {
export interface TableUpdatedEvent extends BaseEvent { export interface TableUpdatedEvent extends BaseEvent {
tableId: string tableId: string
defaultValues: boolean | undefined
aiColumn: AIOperationEnum | undefined
audited: { audited: {
name: string name: string
} }

View File

@ -1,7 +1,9 @@
import { ViewCalculation } from "../../documents" import { CalculationType, ViewCalculation, ViewV2Type } from "../../documents"
import { BaseEvent, TableExportFormat } from "./event" import { BaseEvent, TableExportFormat } from "./event"
export interface ViewCreatedEvent extends BaseEvent { export interface ViewCreatedEvent extends BaseEvent {
name: string
type?: ViewV2Type
tableId: string tableId: string
} }
@ -20,10 +22,12 @@ export interface ViewExportedEvent extends BaseEvent {
export interface ViewFilterCreatedEvent extends BaseEvent { export interface ViewFilterCreatedEvent extends BaseEvent {
tableId: string tableId: string
filterGroups: number
} }
export interface ViewFilterUpdatedEvent extends BaseEvent { export interface ViewFilterUpdatedEvent extends BaseEvent {
tableId: string tableId: string
filterGroups: number
} }
export interface ViewFilterDeletedEvent extends BaseEvent { export interface ViewFilterDeletedEvent extends BaseEvent {
@ -32,7 +36,7 @@ export interface ViewFilterDeletedEvent extends BaseEvent {
export interface ViewCalculationCreatedEvent extends BaseEvent { export interface ViewCalculationCreatedEvent extends BaseEvent {
tableId: string tableId: string
calculation: ViewCalculation calculation: CalculationType
} }
export interface ViewCalculationUpdatedEvent extends BaseEvent { export interface ViewCalculationUpdatedEvent extends BaseEvent {
@ -44,3 +48,7 @@ export interface ViewCalculationDeletedEvent extends BaseEvent {
tableId: string tableId: string
calculation: ViewCalculation calculation: ViewCalculation
} }
export interface ViewJoinCreatedEvent extends BaseEvent {
tableId: string
}