From cdf7cb9fab74f6a76d6bb3cb3492c2213fa74bf1 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 15 Nov 2024 17:54:14 +0000 Subject: [PATCH] Final update to view mapping and typing. --- .../server/src/api/controllers/public/rows.ts | 10 +-- .../src/api/controllers/public/views.ts | 86 +++++++++++-------- .../server/src/api/controllers/row/views.ts | 1 + packages/server/src/api/routes/public/rows.ts | 4 +- .../src/api/routes/public/tests/Request.ts | 19 ++-- .../src/api/routes/public/tests/views.spec.ts | 29 ++++--- .../server/src/api/routes/utils/validators.ts | 43 +++++++++- 7 files changed, 124 insertions(+), 68 deletions(-) diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index 634a41ed85..24ac18c7b1 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -47,12 +47,10 @@ export async function search(ctx: UserCtx, next: Next) { export async function viewSearch(ctx: UserCtx, next: Next) { ctx.request.body = getSearchParameters(ctx) - await rowController.views.searchView({ - ...ctx, - params: { - viewId: ctx.params.viewId, - }, - }) + ctx.params = { + viewId: ctx.params.viewId, + } + await rowController.views.searchView(ctx) await next() } diff --git a/packages/server/src/api/controllers/public/views.ts b/packages/server/src/api/controllers/public/views.ts index 238b46e366..5b08f39e36 100644 --- a/packages/server/src/api/controllers/public/views.ts +++ b/packages/server/src/api/controllers/public/views.ts @@ -1,72 +1,88 @@ import { search as stringSearch } from "./utils" import * as controller from "../view" -import { ViewV2, UserCtx } from "@budibase/types" +import { ViewV2, UserCtx, UISearchFilter, PublicAPIView } from "@budibase/types" import { Next } from "koa" import { merge } from "lodash" -function fixView(view: ViewV2, params?: { viewId: string }) { - if (!params || !view) { - return view +function viewRequest(view: PublicAPIView, params?: { viewId: string }) { + const viewV2: ViewV2 = view + if (!viewV2) { + return viewV2 } if (params?.viewId) { - view.id = params.viewId + viewV2.id = params.viewId } if (!view.query) { - view.query = {} + viewV2.query = {} + } else { + // public API only has one form of query + viewV2.queryUI = viewV2.query as UISearchFilter } - view.version = 2 - return view + viewV2.version = 2 + return viewV2 +} + +function viewResponse(view: ViewV2): PublicAPIView { + // remove our internal structure - always un-necessary + delete view.query + return { + ...view, + query: view.queryUI, + } +} + +function viewsResponse(views: ViewV2[]): PublicAPIView[] { + return views.map(viewResponse) } export async function search(ctx: UserCtx, next: Next) { const { name } = ctx.request.body await controller.v2.fetch(ctx) - ctx.body = stringSearch(ctx.body.data, name) + ctx.body.data = viewsResponse(stringSearch(ctx.body.data, name)) await next() } export async function create(ctx: UserCtx, next: Next) { - await controller.v2.create( - merge(ctx, { - request: { - body: fixView(ctx.request.body), - }, - }) - ) + ctx = merge(ctx, { + request: { + body: viewRequest(ctx.request.body), + }, + }) + await controller.v2.create(ctx) + ctx.body.data = viewResponse(ctx.body.data) await next() } export async function read(ctx: UserCtx, next: Next) { - await controller.v2.get( - merge(ctx, { - params: { - viewId: ctx.params.viewId, - }, - }) - ) + ctx = merge(ctx, { + params: { + viewId: ctx.params.viewId, + }, + }) + await controller.v2.get(ctx) + ctx.body.data = viewResponse(ctx.body.data) await next() } export async function update(ctx: UserCtx, next: Next) { const viewId = ctx.params.viewId - await controller.v2.update( - merge(ctx, { - request: { - body: { - data: fixView(ctx.request.body, { viewId }), - }, + ctx = merge(ctx, { + request: { + body: { + data: viewRequest(ctx.request.body, { viewId }), }, - params: { - viewId, - }, - }) - ) + }, + params: { + viewId, + }, + }) + await controller.v2.update(ctx) + ctx.body.data = viewResponse(ctx.body.data) await next() } export async function destroy(ctx: UserCtx, next: Next) { await controller.v2.remove(ctx) - ctx.body = ctx.table await next() } diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index b8d01424f2..02ac871de0 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -50,6 +50,7 @@ export async function searchView( result.rows.forEach(r => (r._viewId = view.id)) ctx.body = result } + function getSortOptions(request: SearchViewRowRequest, view: ViewV2) { if (request.sort) { return { diff --git a/packages/server/src/api/routes/public/rows.ts b/packages/server/src/api/routes/public/rows.ts index 2085c5cf0f..2fb81d4601 100644 --- a/packages/server/src/api/routes/public/rows.ts +++ b/packages/server/src/api/routes/public/rows.ts @@ -1,4 +1,4 @@ -import controller from "../../controllers/public/rows" +import controller, { viewSearch } from "../../controllers/public/rows" import Endpoint from "./utils/Endpoint" import { externalSearchValidator } from "../utils/validators" @@ -200,7 +200,7 @@ read.push( new Endpoint( "post", "/views/:viewId/rows/search", - controller.search + controller.viewSearch ).addMiddleware(externalSearchValidator()) ) diff --git a/packages/server/src/api/routes/public/tests/Request.ts b/packages/server/src/api/routes/public/tests/Request.ts index 56d93340f7..463d56f141 100644 --- a/packages/server/src/api/routes/public/tests/Request.ts +++ b/packages/server/src/api/routes/public/tests/Request.ts @@ -6,6 +6,7 @@ import { ViewV2Schema, ViewV2, ViewV2Type, + PublicAPIView, } from "@budibase/types" import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils" import TestConfiguration from "../../../../tests/utilities/TestConfiguration" @@ -148,17 +149,17 @@ export class PublicViewAPI { } async create( - view: Omit, + view: Omit, expectations?: PublicAPIExpectations - ): Promise> { + ): Promise> { return this.request.send("post", "/views", view, expectations) } async update( viewId: string, - view: Omit, + view: Omit, expectations?: PublicAPIExpectations - ): Promise> { + ): Promise> { return this.request.send("put", `/views/${viewId}`, view, expectations) } @@ -177,20 +178,14 @@ export class PublicViewAPI { async find( viewId: string, expectations?: PublicAPIExpectations - ): Promise> { + ): Promise> { return this.request.send("get", `/views/${viewId}`, undefined, expectations) } - async fetch( - expectations?: PublicAPIExpectations - ): Promise> { - return this.request.send("get", "/views", undefined, expectations) - } - async search( viewName: string, expectations?: PublicAPIExpectations - ): Promise> { + ): Promise> { return this.request.send( "post", "/views/search", diff --git a/packages/server/src/api/routes/public/tests/views.spec.ts b/packages/server/src/api/routes/public/tests/views.spec.ts index ef2074b1a3..1cbc383f7e 100644 --- a/packages/server/src/api/routes/public/tests/views.spec.ts +++ b/packages/server/src/api/routes/public/tests/views.spec.ts @@ -1,6 +1,6 @@ import * as setup from "../../tests/utilities" import { basicTable } from "../../../../tests/utilities/structures" -import { Table } from "@budibase/types" +import { BasicOperator, Table, UILogicalOperator } from "@budibase/types" import { PublicAPIRequest } from "./Request" import { generator } from "@budibase/backend-core/tests" @@ -34,14 +34,10 @@ describe("check public API security", () => { it("should be able to update a view", async () => { const view = await request.views.create(baseView(), { status: 201 }) - await request.views.update( - view.data.id, - { - ...view.data, - name: "new name", - }, - { status: 200 } - ) + const response = await request.views.update(view.data.id, { + ...view.data, + name: "new name", + }) }) it("should be able to search views", async () => { @@ -77,9 +73,18 @@ describe("check public API security", () => { { ...baseView(), query: { - string: { - name: "hello", - }, + logicalOperator: UILogicalOperator.ANY, + groups: [ + { + filters: [ + { + operator: BasicOperator.STRING, + field: "name", + value: "hello", + }, + ], + }, + ], }, }, { status: 201 } diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index f07f4ff29f..3bee4f88ce 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -12,6 +12,10 @@ import { ViewV2Type, SortOrder, SortType, + UILogicalOperator, + BasicOperator, + ArrayOperator, + RangeOperator, } from "@budibase/types" import Joi, { CustomValidator } from "joi" import { ValidSnippetNameRegex, helpers } from "@budibase/shared-core" @@ -69,6 +73,43 @@ export function tableValidator() { ) } +function searchUIFilterValidator() { + const logicalOperator = Joi.string().valid( + ...Object.values(UILogicalOperator) + ) + const operators = [ + ...Object.values(BasicOperator), + ...Object.values(ArrayOperator), + ...Object.values(RangeOperator), + ] + const filters = Joi.array().items( + Joi.object({ + operator: Joi.string() + .valid(...operators) + .required(), + field: Joi.string().required(), + // could do with better validation of value based on operator + value: Joi.any().required(), + }) + ) + return Joi.object({ + logicalOperator, + onEmptyFilter: Joi.string().valid(...Object.values(EmptyFilterOption)), + groups: Joi.array().items( + Joi.object({ + logicalOperator, + filters, + groups: Joi.array().items( + Joi.object({ + filters, + logicalOperator, + }) + ), + }) + ), + }) +} + export function viewValidator() { return auth.joiValidator.body( Joi.object({ @@ -78,7 +119,7 @@ export function viewValidator() { type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION), primaryDisplay: OPTIONAL_STRING, schema: Joi.object().required(), - query: searchFiltersValidator().optional(), + query: searchUIFilterValidator().optional(), sort: Joi.object({ field: Joi.string().required(), order: Joi.string()