From 67b814a5cadc73ba87fa97ffa72c5b432b5da27e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 21 Oct 2024 13:35:53 +0100 Subject: [PATCH] Yet more tests. --- .../src/api/routes/tests/search.spec.ts | 2 +- .../src/api/routes/tests/viewV2.spec.ts | 238 +++++++++++++++++- packages/server/src/sdk/app/tables/getters.ts | 20 ++ packages/server/src/sdk/app/views/external.ts | 1 + packages/server/src/sdk/app/views/internal.ts | 2 + packages/server/src/sdk/app/views/utils.ts | 8 +- .../server/src/tests/utilities/api/viewV2.ts | 4 +- 7 files changed, 264 insertions(+), 11 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index f9032b8b61..0aa2a80afe 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -2192,7 +2192,7 @@ describe.each([ }) describe("contains", () => { - it.only("successfully finds a row", async () => { + it("successfully finds a row", async () => { await expectQuery({ contains: { users: [user1._id] }, }).toContainExactly([ diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 8104d124c1..a061a370ba 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -39,7 +39,7 @@ import { generator, mocks } from "@budibase/backend-core/tests" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" -import { db, roles, features } from "@budibase/backend-core" +import { db, roles, features, context } from "@budibase/backend-core" describe.each([ ["lucene", undefined], @@ -56,7 +56,8 @@ describe.each([ const isInternal = isSqs || isLucene let table: Table - let datasource: Datasource + let rawDatasource: Datasource | undefined + let datasource: Datasource | undefined let envCleanup: (() => void) | undefined function saveTableRequest( @@ -113,8 +114,9 @@ describe.each([ }) if (dsProvider) { + rawDatasource = await dsProvider datasource = await config.createDatasource({ - datasource: await dsProvider, + datasource: rawDatasource, }) } table = await config.api.table.save(priceTable()) @@ -923,6 +925,7 @@ describe.each([ describe("update", () => { let view: ViewV2 + let table: Table beforeEach(async () => { table = await config.api.table.save(priceTable()) @@ -955,6 +958,21 @@ describe.each([ query: [ { operator: "equal", field: "newField", value: "thatValue" }, ], + // Should also update queryUI because query was not previously set. + queryUI: { + groups: [ + { + logicalOperator: "all", + filters: [ + { + operator: "equal", + field: "newField", + value: "thatValue", + }, + ], + }, + ], + }, schema: expect.anything(), }, }) @@ -974,8 +992,8 @@ describe.each([ query: [ { operator: BasicOperator.EQUAL, - field: generator.word(), - value: generator.word(), + field: "newField", + value: "newValue", }, ], sort: { @@ -999,6 +1017,21 @@ describe.each([ expect((await config.api.table.get(tableId)).views).toEqual({ [view.name]: { ...updatedData, + // queryUI gets generated from query + queryUI: { + groups: [ + { + logicalOperator: "all", + filters: [ + { + operator: "equal", + field: "newField", + value: "newValue", + }, + ], + }, + ], + }, schema: { ...table.schema, id: expect.objectContaining({ @@ -1244,6 +1277,145 @@ describe.each([ ) }) + it("can update queryUI field and query gets regenerated", async () => { + await config.api.viewV2.update({ + ...view, + queryUI: { + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }, + ], + }, + }) + + let updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toEqual({ + $and: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { field: "value" }, + }, + ], + }, + }, + ], + }, + }) + + await config.api.viewV2.update({ + ...updatedView, + queryUI: { + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "newField", + value: "newValue", + }, + ], + }, + ], + }, + }) + + updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toEqual({ + $and: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { newField: "newValue" }, + }, + ], + }, + }, + ], + }, + }) + }) + + it("can delete either query and it will get regenerated from queryUI", async () => { + await config.api.viewV2.update({ + ...view, + query: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }) + + let updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.queryUI).toBeDefined() + + await config.api.viewV2.update({ + ...updatedView, + query: undefined, + }) + + updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toBeDefined() + }) + + // This is because the conversion from queryUI -> query loses data, so you + // can't accurately reproduce the original queryUI from the query. If + // query is a LegacyFilter[] we allow it, because for Budibase v3 + // everything in the db had query set to a LegacyFilter[], and there's no + // loss of information converting from a LegacyFilter[] to a + // UISearchFilter. But we convert to a SearchFilters and that can't be + // accurately converted to a UISearchFilter. + it("can't regenerate queryUI from a query once it has been generated from a queryUI", async () => { + await config.api.viewV2.update({ + ...view, + queryUI: { + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }, + ], + }, + }) + + let updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toBeDefined() + + await config.api.viewV2.update( + { + ...updatedView, + queryUI: undefined, + }, + { + status: 400, + body: { + message: "view is missing queryUI field", + }, + } + ) + }) + !isLucene && describe("calculation views", () => { let table: Table @@ -1597,6 +1769,62 @@ describe.each([ expect.objectContaining({ visible: true, readonly: true }) ) }) + + it("should fill in the queryUI field if it's missing", async () => { + const res = await config.api.viewV2.create({ + name: generator.name(), + tableId: tableId, + query: [ + { + operator: BasicOperator.EQUAL, + field: "one", + value: "1", + }, + ], + schema: { + id: { visible: true }, + one: { visible: true }, + }, + }) + + const table = await config.api.table.get(tableId) + const rawView = table.views![res.name] as ViewV2 + delete rawView.queryUI + + await context.doInAppContext(config.getAppId(), async () => { + const db = context.getAppDB() + + if (!rawDatasource) { + await db.put(table) + } else { + const ds = await config.api.datasource.get(datasource!._id!) + ds.entities![table.name] = table + const updatedDs = { + ...rawDatasource, + _id: ds._id, + _rev: ds._rev, + entities: ds.entities, + } + await db.put(updatedDs) + } + }) + + const view = await getDelegate(res) + expect(view.queryUI).toEqual({ + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "one", + value: "1", + }, + ], + }, + ], + }) + }) }) describe("updating table schema", () => { diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts index 49944bce85..fa87d3cfad 100644 --- a/packages/server/src/sdk/app/tables/getters.ts +++ b/packages/server/src/sdk/app/tables/getters.ts @@ -12,9 +12,26 @@ import { TableResponse, TableSourceType, TableViewsResponse, + View, + ViewV2, } from "@budibase/types" import datasources from "../datasources" import sdk from "../../../sdk" +import { ensureQueryUISet } from "../views/utils" +import { isV2 } from "../views" + +function processView(view: ViewV2 | View) { + if (!isV2(view)) { + return + } + ensureQueryUISet(view) +} + +function processViews(views: (ViewV2 | View)[]) { + for (const view of views) { + processView(view) + } +} export async function processTable(table: Table): Promise { if (!table) { @@ -22,6 +39,9 @@ export async function processTable(table: Table): Promise
{ } table = { ...table } + if (table.views) { + processViews(Object.values(table.views)) + } if (table._id && isExternalTableID(table._id)) { // Old created external tables via Budibase might have a missing field name breaking some UI such as filters if (table.schema["id"] && !table.schema["id"].name) { diff --git a/packages/server/src/sdk/app/views/external.ts b/packages/server/src/sdk/app/views/external.ts index 2484932232..74f2248f58 100644 --- a/packages/server/src/sdk/app/views/external.ts +++ b/packages/server/src/sdk/app/views/external.ts @@ -81,6 +81,7 @@ export async function update(tableId: string, view: ViewV2): Promise { } ensureQuerySet(view) + ensureQueryUISet(view) delete views[existingView.name] views[view.name] = view diff --git a/packages/server/src/sdk/app/views/internal.ts b/packages/server/src/sdk/app/views/internal.ts index fc6b59fefe..efac4a9f8a 100644 --- a/packages/server/src/sdk/app/views/internal.ts +++ b/packages/server/src/sdk/app/views/internal.ts @@ -5,6 +5,7 @@ import sdk from "../../../sdk" import * as utils from "../../../db/utils" import { enrichSchema, isV2 } from "." import { ensureQuerySet, ensureQueryUISet } from "./utils" +import { processTable } from "../tables/getters" export async function get(viewId: string): Promise { const { tableId } = utils.extractViewInfoFromID(viewId) @@ -70,6 +71,7 @@ export async function update(tableId: string, view: ViewV2): Promise { } ensureQuerySet(view) + ensureQueryUISet(view) delete table.views[existingView.name] table.views[view.name] = view diff --git a/packages/server/src/sdk/app/views/utils.ts b/packages/server/src/sdk/app/views/utils.ts index a110acf072..ec942c617a 100644 --- a/packages/server/src/sdk/app/views/utils.ts +++ b/packages/server/src/sdk/app/views/utils.ts @@ -1,6 +1,7 @@ import { ViewV2 } from "@budibase/types" import { utils, dataFilters } from "@budibase/shared-core" import { isPlainObject } from "lodash" +import { HTTPError } from "@budibase/backend-core" function isEmptyObject(obj: any) { return obj && isPlainObject(obj) && Object.keys(obj).length === 0 @@ -21,7 +22,7 @@ export function ensureQueryUISet(view: ViewV2) { // So despite the type saying that `view.query` is a LegacyFilter[] | // SearchFilters, it will never be a SearchFilters when a `view.queryUI` // is specified, making it "safe" to throw an error here. - throw new Error("view is missing queryUI field") + throw new HTTPError("view is missing queryUI field", 400) } view.queryUI = utils.processSearchFilters(view.query) @@ -29,7 +30,10 @@ export function ensureQueryUISet(view: ViewV2) { } export function ensureQuerySet(view: ViewV2) { - if (!view.query && view.queryUI && !isEmptyObject(view.queryUI)) { + // We consider queryUI to be the source of truth, so we don't check for the + // presence of query here. We will overwrite it regardless of whether it is + // present or not. + if (view.queryUI && !isEmptyObject(view.queryUI)) { view.query = dataFilters.buildQuery(view.queryUI) } } diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index ba07dbe5f0..9741240f27 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -10,9 +10,7 @@ import { Expectations, TestAPI } from "./base" export class ViewV2API extends TestAPI { create = async ( - // The frontend changed in v3 from sending query to sending only queryUI, - // making sure tests reflect that. - view: Omit, + view: CreateViewRequest, expectations?: Expectations ): Promise => { const exp: Expectations = {