Merge master.
This commit is contained in:
commit
76b5190454
|
@ -13,9 +13,7 @@ const EXCLUDED_EVENTS: Event[] = [
|
|||
Event.ROLE_UPDATED,
|
||||
Event.DATASOURCE_UPDATED,
|
||||
Event.QUERY_UPDATED,
|
||||
Event.TABLE_UPDATED,
|
||||
Event.VIEW_UPDATED,
|
||||
Event.VIEW_FILTER_UPDATED,
|
||||
Event.VIEW_CALCULATION_UPDATED,
|
||||
Event.AUTOMATION_TRIGGER_UPDATED,
|
||||
Event.USER_GROUP_UPDATED,
|
||||
|
|
|
@ -23,3 +23,4 @@ export { default as plugin } from "./plugin"
|
|||
export { default as backup } from "./backup"
|
||||
export { default as environmentVariable } from "./environmentVariable"
|
||||
export { default as auditLog } from "./auditLog"
|
||||
export { default as rowAction } from "./rowAction"
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import { publishEvent } from "../events"
|
||||
import {
|
||||
Event,
|
||||
TableExportFormat,
|
||||
FieldType,
|
||||
Table,
|
||||
TableCreatedEvent,
|
||||
TableUpdatedEvent,
|
||||
TableDeletedEvent,
|
||||
TableExportedEvent,
|
||||
TableExportFormat,
|
||||
TableImportedEvent,
|
||||
TableUpdatedEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 = {
|
||||
tableId: table._id as string,
|
||||
tableId: newTable._id as string,
|
||||
defaultValues,
|
||||
aiColumn,
|
||||
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) {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { publishEvent } from "../events"
|
||||
import {
|
||||
CalculationType,
|
||||
Event,
|
||||
Table,
|
||||
TableExportFormat,
|
||||
View,
|
||||
ViewCalculation,
|
||||
ViewCalculationCreatedEvent,
|
||||
ViewCalculationDeletedEvent,
|
||||
ViewCalculationUpdatedEvent,
|
||||
|
@ -11,20 +16,20 @@ import {
|
|||
ViewFilterDeletedEvent,
|
||||
ViewFilterUpdatedEvent,
|
||||
ViewUpdatedEvent,
|
||||
View,
|
||||
ViewCalculation,
|
||||
Table,
|
||||
TableExportFormat,
|
||||
ViewV2,
|
||||
ViewJoinCreatedEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
async function created(view: View, timestamp?: string | number) {
|
||||
async function created(view: ViewV2, timestamp?: string | number) {
|
||||
const properties: ViewCreatedEvent = {
|
||||
name: view.name,
|
||||
type: view.type,
|
||||
tableId: view.tableId,
|
||||
}
|
||||
await publishEvent(Event.VIEW_CREATED, properties, timestamp)
|
||||
}
|
||||
|
||||
async function updated(view: View) {
|
||||
async function updated(view: ViewV2) {
|
||||
const properties: ViewUpdatedEvent = {
|
||||
tableId: view.tableId,
|
||||
}
|
||||
|
@ -46,16 +51,27 @@ async function exported(table: Table, format: TableExportFormat) {
|
|||
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 = {
|
||||
tableId: view.tableId,
|
||||
tableId,
|
||||
filterGroups,
|
||||
}
|
||||
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 = {
|
||||
tableId: view.tableId,
|
||||
tableId: tableId,
|
||||
filterGroups,
|
||||
}
|
||||
await publishEvent(Event.VIEW_FILTER_UPDATED, properties)
|
||||
}
|
||||
|
@ -67,10 +83,16 @@ async function filterDeleted(view: View) {
|
|||
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 = {
|
||||
tableId: view.tableId,
|
||||
calculation: view.calculation as ViewCalculation,
|
||||
tableId,
|
||||
calculation: calculationType,
|
||||
}
|
||||
await publishEvent(Event.VIEW_CALCULATION_CREATED, properties, timestamp)
|
||||
}
|
||||
|
@ -91,6 +113,13 @@ async function calculationDeleted(existingView: View) {
|
|||
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 {
|
||||
created,
|
||||
updated,
|
||||
|
@ -102,4 +131,5 @@ export default {
|
|||
calculationCreated,
|
||||
calculationUpdated,
|
||||
calculationDeleted,
|
||||
viewJoinCreated,
|
||||
}
|
||||
|
|
|
@ -117,6 +117,7 @@ beforeAll(async () => {
|
|||
jest.spyOn(events.view, "calculationCreated")
|
||||
jest.spyOn(events.view, "calculationUpdated")
|
||||
jest.spyOn(events.view, "calculationDeleted")
|
||||
jest.spyOn(events.view, "viewJoinCreated")
|
||||
|
||||
jest.spyOn(events.plugin, "init")
|
||||
jest.spyOn(events.plugin, "imported")
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
} else {
|
||||
// Leave the core data as it is
|
||||
return testData
|
||||
return cloneDeep(testData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,10 @@
|
|||
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
|
||||
|
@ -85,7 +88,7 @@
|
|||
required => testData?.[required] || required !== "row"
|
||||
)
|
||||
|
||||
function parseTestJSON(e) {
|
||||
async function parseTestJSON(e) {
|
||||
let jsonUpdate
|
||||
|
||||
try {
|
||||
|
@ -105,7 +108,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
automationStore.actions.addTestDataToAutomation(jsonUpdate)
|
||||
const updatedAuto =
|
||||
automationStore.actions.addTestDataToAutomation(jsonUpdate)
|
||||
await automationStore.actions.save(updatedAuto)
|
||||
}
|
||||
|
||||
const testAutomation = async () => {
|
||||
|
@ -150,10 +155,14 @@
|
|||
{#if selectedValues}
|
||||
<div class="tab-content-padding">
|
||||
<AutomationBlockSetup
|
||||
bind:testData
|
||||
{schemaProperties}
|
||||
isTestModal
|
||||
{testData}
|
||||
block={trigger}
|
||||
on:update={e => {
|
||||
const { testData: updatedTestData } = e.detail
|
||||
testData = updatedTestData
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -162,7 +171,7 @@
|
|||
<TextArea
|
||||
value={JSON.stringify($selectedAutomation.data.testData, null, 2)}
|
||||
error={failedParse}
|
||||
on:change={e => parseTestJSON(e)}
|
||||
on:change={async e => await parseTestJSON(e)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
|
||||
import { getSchemaForDatasourcePlus } from "dataBinding"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
|
@ -67,6 +67,8 @@
|
|||
export let isTestModal = false
|
||||
export let bindings = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Stop unnecessary rendering
|
||||
const memoBlock = memo(block)
|
||||
|
||||
|
@ -503,15 +505,7 @@
|
|||
row: { "Active": true, "Order Id" : 14, ... }
|
||||
})
|
||||
*/
|
||||
const onChange = async update => {
|
||||
if (isTestModal) {
|
||||
testData = update
|
||||
}
|
||||
|
||||
updateAutomation(update)
|
||||
}
|
||||
|
||||
const updateAutomation = Utils.sequential(async update => {
|
||||
const onChange = Utils.sequential(async update => {
|
||||
const request = cloneDeep(update)
|
||||
// Process app trigger updates
|
||||
if (isTrigger && !isTestModal) {
|
||||
|
@ -540,7 +534,9 @@
|
|||
}
|
||||
try {
|
||||
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
|
||||
if (stepId === TriggerStepID.WEBHOOK) {
|
||||
|
@ -557,7 +553,13 @@
|
|||
...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 {
|
||||
const data = { schema, ...request }
|
||||
await automationStore.actions.updateBlockInputs(block, data)
|
||||
|
|
|
@ -880,13 +880,13 @@ const automationActions = store => ({
|
|||
appId,
|
||||
})
|
||||
},
|
||||
addTestDataToAutomation: async data => {
|
||||
addTestDataToAutomation: data => {
|
||||
let newAutomation = cloneDeep(get(selectedAutomation).data)
|
||||
newAutomation.testData = {
|
||||
...newAutomation.testData,
|
||||
...data,
|
||||
}
|
||||
await store.actions.save(newAutomation)
|
||||
return newAutomation
|
||||
},
|
||||
constructBlock(type, stepId, blockDefinition) {
|
||||
let newName
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
RowActionResponse,
|
||||
RowActionsResponse,
|
||||
} from "@budibase/types"
|
||||
import { events } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
|
||||
async function getTable(ctx: Ctx) {
|
||||
|
@ -59,6 +60,8 @@ export async function create(
|
|||
name: ctx.request.body.name,
|
||||
})
|
||||
|
||||
await events.rowAction.created(createdAction)
|
||||
|
||||
ctx.body = {
|
||||
tableId,
|
||||
id: createdAction.id,
|
||||
|
|
|
@ -45,13 +45,13 @@ export async function updateTable(
|
|||
inputs.created = true
|
||||
}
|
||||
try {
|
||||
const { datasource, table } = await sdk.tables.external.save(
|
||||
const { datasource, oldTable, table } = await sdk.tables.external.save(
|
||||
datasourceId!,
|
||||
inputs,
|
||||
{ tableId, renaming }
|
||||
)
|
||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||
return table
|
||||
return { table, oldTable }
|
||||
} catch (err: any) {
|
||||
if (err instanceof Error) {
|
||||
ctx.throw(400, err.message)
|
||||
|
|
|
@ -120,8 +120,15 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
|||
await events.table.created(savedTable)
|
||||
} else {
|
||||
const api = pickApi({ table })
|
||||
savedTable = await api.updateTable(ctx, renaming)
|
||||
await events.table.updated(savedTable)
|
||||
const { table: updatedTable, oldTable } = await api.updateTable(
|
||||
ctx,
|
||||
renaming
|
||||
)
|
||||
savedTable = updatedTable
|
||||
|
||||
if (oldTable) {
|
||||
await events.table.updated(oldTable, savedTable)
|
||||
}
|
||||
}
|
||||
if (renaming) {
|
||||
await sdk.views.renameLinkedViews(savedTable, renaming)
|
||||
|
|
|
@ -30,14 +30,14 @@ export async function updateTable(
|
|||
}
|
||||
|
||||
try {
|
||||
const { table } = await sdk.tables.internal.save(tableToSave, {
|
||||
const { table, oldTable } = await sdk.tables.internal.save(tableToSave, {
|
||||
userId: ctx.user._id,
|
||||
rowsToImport: rows,
|
||||
tableId: ctx.request.body._id,
|
||||
renaming,
|
||||
})
|
||||
|
||||
return table
|
||||
return { table, oldTable }
|
||||
} catch (err: any) {
|
||||
if (err instanceof Error) {
|
||||
ctx.throw(400, err.message)
|
||||
|
|
|
@ -19,8 +19,6 @@ import { builderSocket } from "../../../websockets"
|
|||
|
||||
const cloneDeep = require("lodash/cloneDeep")
|
||||
|
||||
import isEqual from "lodash/isEqual"
|
||||
|
||||
export async function fetch(ctx: Ctx) {
|
||||
ctx.body = await getViews()
|
||||
}
|
||||
|
@ -60,71 +58,11 @@ export async function save(ctx: Ctx) {
|
|||
existingTable.views[viewName] = existingTable.views[originalName]
|
||||
}
|
||||
await db.put(table)
|
||||
await handleViewEvents(
|
||||
existingTable.views[viewName] as View,
|
||||
table.views[viewName]
|
||||
)
|
||||
|
||||
ctx.body = table.views[viewName]
|
||||
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) {
|
||||
const db = context.getAppDB()
|
||||
const viewName = decodeURIComponent(ctx.params.viewName)
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
CreateViewResponse,
|
||||
UpdateViewResponse,
|
||||
} from "@budibase/types"
|
||||
import { events } from "@budibase/backend-core"
|
||||
import { builderSocket, gridSocket } from "../../../websockets"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
|
@ -150,6 +151,9 @@ export async function create(ctx: Ctx<CreateViewRequest, CreateViewResponse>) {
|
|||
primaryDisplay: view.primaryDisplay,
|
||||
}
|
||||
const result = await sdk.views.create(tableId, parsedView)
|
||||
|
||||
await events.view.created(result)
|
||||
|
||||
ctx.status = 201
|
||||
ctx.body = {
|
||||
data: result,
|
||||
|
@ -160,6 +164,46 @@ export async function create(ctx: Ctx<CreateViewRequest, CreateViewResponse>) {
|
|||
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>) {
|
||||
const view = ctx.request.body
|
||||
|
||||
|
@ -187,10 +231,15 @@ export async function update(ctx: Ctx<UpdateViewRequest, UpdateViewResponse>) {
|
|||
primaryDisplay: view.primaryDisplay,
|
||||
}
|
||||
|
||||
const result = await sdk.views.update(tableId, parsedView)
|
||||
ctx.body = {
|
||||
data: result,
|
||||
}
|
||||
const { view: result, existingView } = await sdk.views.update(
|
||||
tableId,
|
||||
parsedView
|
||||
)
|
||||
|
||||
await handleViewEvents(existingView, result)
|
||||
await events.view.updated(result)
|
||||
|
||||
ctx.body = { data: result }
|
||||
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
builderSocket?.emitTableUpdate(ctx, table)
|
||||
|
|
|
@ -247,6 +247,9 @@ if (descriptions.length) {
|
|||
},
|
||||
},
|
||||
},
|
||||
primary: ["_id"],
|
||||
views: {},
|
||||
sql: true,
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -254,9 +257,8 @@ if (descriptions.length) {
|
|||
...table,
|
||||
name: generator.guid(),
|
||||
})
|
||||
|
||||
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 () => {
|
||||
|
|
|
@ -73,25 +73,12 @@ describe("/views", () => {
|
|||
}
|
||||
|
||||
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 () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
const view = await saveView({ calculation: ViewCalculation.COUNT })
|
||||
|
||||
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 () => {
|
||||
|
@ -109,14 +96,6 @@ describe("/views", () => {
|
|||
})
|
||||
|
||||
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 () => {
|
||||
|
@ -166,13 +145,6 @@ describe("/views", () => {
|
|||
await saveView()
|
||||
|
||||
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 () => {
|
||||
|
@ -182,13 +154,6 @@ describe("/views", () => {
|
|||
await saveView({ calculation: ViewCalculation.COUNT })
|
||||
|
||||
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 () => {
|
||||
|
@ -198,13 +163,6 @@ describe("/views", () => {
|
|||
await saveView({ calculation: undefined })
|
||||
|
||||
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 () => {
|
||||
|
@ -230,13 +188,6 @@ describe("/views", () => {
|
|||
})
|
||||
|
||||
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 () => {
|
||||
|
@ -254,13 +205,6 @@ describe("/views", () => {
|
|||
await saveView({ filters: [] })
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import {
|
||||
ArrayOperator,
|
||||
BasicOperator,
|
||||
BBReferenceFieldSubType,
|
||||
CalculationType,
|
||||
CreateViewRequest,
|
||||
Datasource,
|
||||
EmptyFilterOption,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
JsonFieldSubType,
|
||||
JsonTypes,
|
||||
LegacyFilter,
|
||||
NumericCalculationFieldMetadata,
|
||||
PermissionLevel,
|
||||
QuotaUsageType,
|
||||
RelationshipType,
|
||||
RenameColumn,
|
||||
Row,
|
||||
SaveTableRequest,
|
||||
SearchFilters,
|
||||
SearchResponse,
|
||||
SearchViewRowRequest,
|
||||
SortOrder,
|
||||
SortType,
|
||||
StaticQuotaName,
|
||||
Table,
|
||||
TableSchema,
|
||||
TableSourceType,
|
||||
UILogicalOperator,
|
||||
UISearchFilter,
|
||||
UpdateViewRequest,
|
||||
ViewV2,
|
||||
SearchResponse,
|
||||
BasicOperator,
|
||||
CalculationType,
|
||||
RelationshipType,
|
||||
TableSchema,
|
||||
RenameColumn,
|
||||
BBReferenceFieldSubType,
|
||||
NumericCalculationFieldMetadata,
|
||||
ViewV2Schema,
|
||||
ViewV2Type,
|
||||
JsonTypes,
|
||||
EmptyFilterOption,
|
||||
JsonFieldSubType,
|
||||
UISearchFilter,
|
||||
LegacyFilter,
|
||||
SearchViewRowRequest,
|
||||
ArrayOperator,
|
||||
UILogicalOperator,
|
||||
SearchFilters,
|
||||
} from "@budibase/types"
|
||||
import { generator, mocks } from "@budibase/backend-core/tests"
|
||||
import {
|
||||
|
@ -42,7 +42,7 @@ import {
|
|||
} from "../../../integrations/tests/utils"
|
||||
import merge from "lodash/merge"
|
||||
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] })
|
||||
|
||||
|
@ -129,6 +129,7 @@ if (descriptions.length) {
|
|||
id: expect.stringMatching(new RegExp(`${table._id!}_`)),
|
||||
version: 2,
|
||||
})
|
||||
expect(events.view.created).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("can persist views with all fields", async () => {
|
||||
|
@ -195,6 +196,7 @@ if (descriptions.length) {
|
|||
}
|
||||
|
||||
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 () => {
|
||||
|
@ -224,6 +226,7 @@ if (descriptions.length) {
|
|||
},
|
||||
}
|
||||
const res = await config.api.viewV2.create(newView)
|
||||
expect(events.view.created).toHaveBeenCalledTimes(1)
|
||||
|
||||
const expected: ViewV2 = {
|
||||
...newView,
|
||||
|
@ -283,6 +286,7 @@ if (descriptions.length) {
|
|||
}
|
||||
|
||||
const createdView = await config.api.viewV2.create(newView)
|
||||
expect(events.view.created).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(createdView).toEqual({
|
||||
...newView,
|
||||
|
@ -990,6 +994,46 @@ if (descriptions.length) {
|
|||
expect((await config.api.table.get(tableId)).views).toEqual({
|
||||
[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 () => {
|
||||
|
@ -1621,6 +1665,7 @@ if (descriptions.length) {
|
|||
field: "age",
|
||||
}
|
||||
await config.api.viewV2.update(view)
|
||||
expect(events.view.calculationCreated).toHaveBeenCalledTimes(1)
|
||||
|
||||
const { rows } = await config.api.row.search(view.id)
|
||||
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 () => {
|
||||
|
@ -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 () => {
|
||||
let auxTable = await createAuxTable()
|
||||
|
||||
|
|
|
@ -7,24 +7,6 @@ export const backfill = async (appDb: Database, timestamp: string | number) => {
|
|||
|
||||
for (const table of tables) {
|
||||
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
|
||||
|
|
|
@ -73,16 +73,12 @@ describe("migrations", () => {
|
|||
expect(events.query.created).toHaveBeenCalledTimes(2)
|
||||
expect(events.role.created).toHaveBeenCalledTimes(3) // created roles + admin (created on table creation)
|
||||
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)
|
||||
|
||||
// to make sure caching is working as expected
|
||||
expect(
|
||||
events.processors.analyticsProcessor.processEvent
|
||||
).toHaveBeenCalledTimes(24) // Addtion of of the events above
|
||||
).toHaveBeenCalledTimes(20) // Addition of of the events above
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -281,7 +281,7 @@ export async function save(
|
|||
tableToSave.sql = true
|
||||
}
|
||||
|
||||
return { datasource: updatedDatasource, table: tableToSave }
|
||||
return { datasource: updatedDatasource, table: tableToSave, oldTable }
|
||||
}
|
||||
|
||||
export async function destroy(datasourceId: string, table: Table) {
|
||||
|
|
|
@ -171,7 +171,7 @@ export async function save(
|
|||
}
|
||||
// has to run after, make sure it has _id
|
||||
await runStaticFormulaChecks(table, { oldTable, deletion: false })
|
||||
return { table }
|
||||
return { table, oldTable }
|
||||
}
|
||||
|
||||
export async function destroy(table: Table) {
|
||||
|
|
|
@ -63,7 +63,7 @@ export async function create(
|
|||
export async function update(
|
||||
tableId: string,
|
||||
view: Readonly<ViewV2>
|
||||
): Promise<ViewV2> {
|
||||
): Promise<{ view: Readonly<ViewV2>; existingView: ViewV2 }> {
|
||||
const db = context.getAppDB()
|
||||
|
||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||
|
@ -87,7 +87,7 @@ export async function update(
|
|||
delete views[existingView.name]
|
||||
views[view.name] = view
|
||||
await db.put(ds)
|
||||
return view
|
||||
return { view, existingView } as { view: ViewV2; existingView: ViewV2 }
|
||||
}
|
||||
|
||||
export async function remove(viewId: string): Promise<ViewV2> {
|
||||
|
|
|
@ -315,7 +315,10 @@ export async function create(
|
|||
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)
|
||||
|
||||
return pickApi(tableId).update(tableId, view)
|
||||
|
|
|
@ -54,7 +54,7 @@ export async function create(
|
|||
export async function update(
|
||||
tableId: string,
|
||||
view: Readonly<ViewV2>
|
||||
): Promise<ViewV2> {
|
||||
): Promise<{ view: ViewV2; existingView: ViewV2 }> {
|
||||
const db = context.getAppDB()
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
table.views ??= {}
|
||||
|
@ -76,7 +76,7 @@ export async function update(
|
|||
delete table.views[existingView.name]
|
||||
table.views[view.name] = view
|
||||
await db.put(table)
|
||||
return view
|
||||
return { view, existingView } as { view: ViewV2; existingView: ViewV2 }
|
||||
}
|
||||
|
||||
export async function remove(viewId: string): Promise<ViewV2> {
|
||||
|
|
|
@ -118,6 +118,7 @@ export enum Event {
|
|||
VIEW_CALCULATION_CREATED = "view:calculation:created",
|
||||
VIEW_CALCULATION_UPDATED = "view:calculation:updated",
|
||||
VIEW_CALCULATION_DELETED = "view:calculation:deleted",
|
||||
VIEW_JOIN_CREATED = "view:join:created",
|
||||
|
||||
// ROWS
|
||||
ROWS_CREATED = "rows:created",
|
||||
|
@ -192,6 +193,9 @@ export enum Event {
|
|||
// AUDIT LOG
|
||||
AUDIT_LOGS_FILTERED = "audit_log:filtered",
|
||||
AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded",
|
||||
|
||||
// ROW ACTION
|
||||
ROW_ACTION_CREATED = "row_action:created",
|
||||
}
|
||||
|
||||
export const UserGroupSyncEvents: Event[] = [
|
||||
|
@ -376,6 +380,7 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
|||
[Event.VIEW_CALCULATION_CREATED]: undefined,
|
||||
[Event.VIEW_CALCULATION_UPDATED]: undefined,
|
||||
[Event.VIEW_CALCULATION_DELETED]: undefined,
|
||||
[Event.VIEW_JOIN_CREATED]: undefined,
|
||||
|
||||
// SERVED - NOT AUDITED
|
||||
[Event.SERVED_BUILDER]: undefined,
|
||||
|
@ -395,6 +400,9 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
|||
// AUDIT LOG - NOT AUDITED
|
||||
[Event.AUDIT_LOGS_FILTERED]: 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
|
||||
|
|
|
@ -24,3 +24,4 @@ export * from "./plugin"
|
|||
export * from "./backup"
|
||||
export * from "./environmentVariable"
|
||||
export * from "./auditLog"
|
||||
export * from "./rowAction"
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { BaseEvent } from "./event"
|
||||
|
||||
export interface RowActionCreatedEvent extends BaseEvent {
|
||||
name: string
|
||||
automationId: string
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { BaseEvent, TableExportFormat } from "./event"
|
||||
import { AIOperationEnum } from "../ai"
|
||||
|
||||
export interface TableCreatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
|
@ -9,6 +10,8 @@ export interface TableCreatedEvent extends BaseEvent {
|
|||
|
||||
export interface TableUpdatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
defaultValues: boolean | undefined
|
||||
aiColumn: AIOperationEnum | undefined
|
||||
audited: {
|
||||
name: string
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { ViewCalculation } from "../../documents"
|
||||
import { CalculationType, ViewCalculation, ViewV2Type } from "../../documents"
|
||||
import { BaseEvent, TableExportFormat } from "./event"
|
||||
|
||||
export interface ViewCreatedEvent extends BaseEvent {
|
||||
name: string
|
||||
type?: ViewV2Type
|
||||
tableId: string
|
||||
}
|
||||
|
||||
|
@ -20,10 +22,12 @@ export interface ViewExportedEvent extends BaseEvent {
|
|||
|
||||
export interface ViewFilterCreatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
filterGroups: number
|
||||
}
|
||||
|
||||
export interface ViewFilterUpdatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
filterGroups: number
|
||||
}
|
||||
|
||||
export interface ViewFilterDeletedEvent extends BaseEvent {
|
||||
|
@ -32,7 +36,7 @@ export interface ViewFilterDeletedEvent extends BaseEvent {
|
|||
|
||||
export interface ViewCalculationCreatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
calculation: ViewCalculation
|
||||
calculation: CalculationType
|
||||
}
|
||||
|
||||
export interface ViewCalculationUpdatedEvent extends BaseEvent {
|
||||
|
@ -44,3 +48,7 @@ export interface ViewCalculationDeletedEvent extends BaseEvent {
|
|||
tableId: string
|
||||
calculation: ViewCalculation
|
||||
}
|
||||
|
||||
export interface ViewJoinCreatedEvent extends BaseEvent {
|
||||
tableId: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue