diff --git a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts index 12d2bb7e2c..6c45da09e6 100644 --- a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts +++ b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts @@ -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, diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 9c92b80499..aaec62f979 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -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" diff --git a/packages/backend-core/src/events/publishers/rowAction.ts b/packages/backend-core/src/events/publishers/rowAction.ts new file mode 100644 index 0000000000..eac35cc489 --- /dev/null +++ b/packages/backend-core/src/events/publishers/rowAction.ts @@ -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, +} diff --git a/packages/backend-core/src/events/publishers/table.ts b/packages/backend-core/src/events/publishers/table.ts index dc3200291a..77a2c3e1a4 100644 --- a/packages/backend-core/src/events/publishers/table.ts +++ b/packages/backend-core/src/events/publishers/table.ts @@ -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) { diff --git a/packages/backend-core/src/events/publishers/view.ts b/packages/backend-core/src/events/publishers/view.ts index ccbf960b04..3ce24e9a0a 100644 --- a/packages/backend-core/src/events/publishers/view.ts +++ b/packages/backend-core/src/events/publishers/view.ts @@ -1,6 +1,11 @@ import { publishEvent } from "../events" import { + CalculationType, Event, + Table, + TableExportFormat, + View, + ViewCalculation, ViewCalculationCreatedEvent, ViewCalculationDeletedEvent, ViewCalculationUpdatedEvent, @@ -11,22 +16,22 @@ import { ViewFilterDeletedEvent, ViewFilterUpdatedEvent, ViewUpdatedEvent, - View, - ViewCalculation, - Table, - TableExportFormat, + ViewV2, + ViewJoinCreatedEvent, } from "@budibase/types" /* eslint-disable */ -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, } @@ -48,16 +53,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) } @@ -69,10 +85,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) } @@ -93,6 +115,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, @@ -104,4 +133,5 @@ export default { calculationCreated, calculationUpdated, calculationDeleted, + viewJoinCreated, } diff --git a/packages/backend-core/tests/core/utilities/mocks/events.ts b/packages/backend-core/tests/core/utilities/mocks/events.ts index 96f351de10..433986352e 100644 --- a/packages/backend-core/tests/core/utilities/mocks/events.ts +++ b/packages/backend-core/tests/core/utilities/mocks/events.ts @@ -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") diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 3baaff6fcc..525fcabcfc 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -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, diff --git a/packages/server/src/api/controllers/table/external.ts b/packages/server/src/api/controllers/table/external.ts index 6f09bf4a61..d3f5ef99f6 100644 --- a/packages/server/src/api/controllers/table/external.ts +++ b/packages/server/src/api/controllers/table/external.ts @@ -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) diff --git a/packages/server/src/api/controllers/table/index.ts b/packages/server/src/api/controllers/table/index.ts index 4b020290e9..96599824c5 100644 --- a/packages/server/src/api/controllers/table/index.ts +++ b/packages/server/src/api/controllers/table/index.ts @@ -120,8 +120,15 @@ export async function save(ctx: UserCtx) { 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) diff --git a/packages/server/src/api/controllers/table/internal.ts b/packages/server/src/api/controllers/table/internal.ts index 40ce5e279d..67c4ec100c 100644 --- a/packages/server/src/api/controllers/table/internal.ts +++ b/packages/server/src/api/controllers/table/internal.ts @@ -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) diff --git a/packages/server/src/api/controllers/view/views.ts b/packages/server/src/api/controllers/view/views.ts index b1f1f6c154..bc734c5657 100644 --- a/packages/server/src/api/controllers/view/views.ts +++ b/packages/server/src/api/controllers/view/views.ts @@ -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) diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index b1433985c1..da579efed7 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -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) { 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) { 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) { const view = ctx.request.body @@ -187,10 +231,15 @@ export async function update(ctx: Ctx) { 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) diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index 8556a598c6..e47181e21f 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -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 () => { diff --git a/packages/server/src/api/routes/tests/view.spec.ts b/packages/server/src/api/routes/tests/view.spec.ts index 57b589e79d..e1968d2899 100644 --- a/packages/server/src/api/routes/tests/view.spec.ts +++ b/packages/server/src/api/routes/tests/view.spec.ts @@ -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) }) }) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 23ae7c79d3..244a0a23eb 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -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() diff --git a/packages/server/src/migrations/functions/backfill/app/tables.ts b/packages/server/src/migrations/functions/backfill/app/tables.ts index 081b81ede5..e8437bd529 100644 --- a/packages/server/src/migrations/functions/backfill/app/tables.ts +++ b/packages/server/src/migrations/functions/backfill/app/tables.ts @@ -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 diff --git a/packages/server/src/migrations/tests/index.spec.ts b/packages/server/src/migrations/tests/index.spec.ts index 6b3f3314ba..3a23d8f011 100644 --- a/packages/server/src/migrations/tests/index.spec.ts +++ b/packages/server/src/migrations/tests/index.spec.ts @@ -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 }) }) }) diff --git a/packages/server/src/sdk/app/tables/external/index.ts b/packages/server/src/sdk/app/tables/external/index.ts index 3eaf146793..7883a912b5 100644 --- a/packages/server/src/sdk/app/tables/external/index.ts +++ b/packages/server/src/sdk/app/tables/external/index.ts @@ -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) { diff --git a/packages/server/src/sdk/app/tables/internal/index.ts b/packages/server/src/sdk/app/tables/internal/index.ts index fbcbed03dc..5b9f346e93 100644 --- a/packages/server/src/sdk/app/tables/internal/index.ts +++ b/packages/server/src/sdk/app/tables/internal/index.ts @@ -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) { diff --git a/packages/server/src/sdk/app/views/external.ts b/packages/server/src/sdk/app/views/external.ts index bee153a910..65e0ff410d 100644 --- a/packages/server/src/sdk/app/views/external.ts +++ b/packages/server/src/sdk/app/views/external.ts @@ -63,7 +63,7 @@ export async function create( export async function update( tableId: string, view: Readonly -): Promise { +): Promise<{ view: Readonly; 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 { diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 58537c96ad..f483ebc0bc 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -315,7 +315,10 @@ export async function create( return view } -export async function update(tableId: string, view: ViewV2): Promise { +export async function update( + tableId: string, + view: ViewV2 +): Promise<{ view: ViewV2; existingView: ViewV2 }> { await guardViewSchema(tableId, view) return pickApi(tableId).update(tableId, view) diff --git a/packages/server/src/sdk/app/views/internal.ts b/packages/server/src/sdk/app/views/internal.ts index 63807bcfd4..4f7abad357 100644 --- a/packages/server/src/sdk/app/views/internal.ts +++ b/packages/server/src/sdk/app/views/internal.ts @@ -54,7 +54,7 @@ export async function create( export async function update( tableId: string, view: Readonly -): Promise { +): 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 { diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 242b182dec..23c0eb0cbd 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -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.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 = { // 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 diff --git a/packages/types/src/sdk/events/index.ts b/packages/types/src/sdk/events/index.ts index 043e62faa4..7a067fd202 100644 --- a/packages/types/src/sdk/events/index.ts +++ b/packages/types/src/sdk/events/index.ts @@ -24,3 +24,4 @@ export * from "./plugin" export * from "./backup" export * from "./environmentVariable" export * from "./auditLog" +export * from "./rowAction" diff --git a/packages/types/src/sdk/events/rowAction.ts b/packages/types/src/sdk/events/rowAction.ts new file mode 100644 index 0000000000..924f11cf76 --- /dev/null +++ b/packages/types/src/sdk/events/rowAction.ts @@ -0,0 +1,6 @@ +import { BaseEvent } from "./event" + +export interface RowActionCreatedEvent extends BaseEvent { + name: string + automationId: string +} diff --git a/packages/types/src/sdk/events/table.ts b/packages/types/src/sdk/events/table.ts index 8df2a95796..4a5880b1db 100644 --- a/packages/types/src/sdk/events/table.ts +++ b/packages/types/src/sdk/events/table.ts @@ -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 } diff --git a/packages/types/src/sdk/events/view.ts b/packages/types/src/sdk/events/view.ts index 452094d2f4..0ea153ad9d 100644 --- a/packages/types/src/sdk/events/view.ts +++ b/packages/types/src/sdk/events/view.ts @@ -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 +}