Final update to view mapping and typing.

This commit is contained in:
mike12345567 2024-11-15 17:54:14 +00:00
parent 82c7c089cb
commit cdf7cb9fab
7 changed files with 124 additions and 68 deletions

View File

@ -47,12 +47,10 @@ export async function search(ctx: UserCtx, next: Next) {
export async function viewSearch(ctx: UserCtx, next: Next) { export async function viewSearch(ctx: UserCtx, next: Next) {
ctx.request.body = getSearchParameters(ctx) ctx.request.body = getSearchParameters(ctx)
await rowController.views.searchView({ ctx.params = {
...ctx, viewId: ctx.params.viewId,
params: { }
viewId: ctx.params.viewId, await rowController.views.searchView(ctx)
},
})
await next() await next()
} }

View File

@ -1,72 +1,88 @@
import { search as stringSearch } from "./utils" import { search as stringSearch } from "./utils"
import * as controller from "../view" import * as controller from "../view"
import { ViewV2, UserCtx } from "@budibase/types" import { ViewV2, UserCtx, UISearchFilter, PublicAPIView } from "@budibase/types"
import { Next } from "koa" import { Next } from "koa"
import { merge } from "lodash" import { merge } from "lodash"
function fixView(view: ViewV2, params?: { viewId: string }) { function viewRequest(view: PublicAPIView, params?: { viewId: string }) {
if (!params || !view) { const viewV2: ViewV2 = view
return view if (!viewV2) {
return viewV2
} }
if (params?.viewId) { if (params?.viewId) {
view.id = params.viewId viewV2.id = params.viewId
} }
if (!view.query) { 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 viewV2.version = 2
return view 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) { export async function search(ctx: UserCtx, next: Next) {
const { name } = ctx.request.body const { name } = ctx.request.body
await controller.v2.fetch(ctx) await controller.v2.fetch(ctx)
ctx.body = stringSearch(ctx.body.data, name) ctx.body.data = viewsResponse(stringSearch(ctx.body.data, name))
await next() await next()
} }
export async function create(ctx: UserCtx, next: Next) { export async function create(ctx: UserCtx, next: Next) {
await controller.v2.create( ctx = merge(ctx, {
merge(ctx, { request: {
request: { body: viewRequest(ctx.request.body),
body: fixView(ctx.request.body), },
}, })
}) await controller.v2.create(ctx)
) ctx.body.data = viewResponse(ctx.body.data)
await next() await next()
} }
export async function read(ctx: UserCtx, next: Next) { export async function read(ctx: UserCtx, next: Next) {
await controller.v2.get( ctx = merge(ctx, {
merge(ctx, { params: {
params: { viewId: ctx.params.viewId,
viewId: ctx.params.viewId, },
}, })
}) await controller.v2.get(ctx)
) ctx.body.data = viewResponse(ctx.body.data)
await next() await next()
} }
export async function update(ctx: UserCtx, next: Next) { export async function update(ctx: UserCtx, next: Next) {
const viewId = ctx.params.viewId const viewId = ctx.params.viewId
await controller.v2.update( ctx = merge(ctx, {
merge(ctx, { request: {
request: { body: {
body: { data: viewRequest(ctx.request.body, { viewId }),
data: fixView(ctx.request.body, { viewId }),
},
}, },
params: { },
viewId, params: {
}, viewId,
}) },
) })
await controller.v2.update(ctx)
ctx.body.data = viewResponse(ctx.body.data)
await next() await next()
} }
export async function destroy(ctx: UserCtx, next: Next) { export async function destroy(ctx: UserCtx, next: Next) {
await controller.v2.remove(ctx) await controller.v2.remove(ctx)
ctx.body = ctx.table
await next() await next()
} }

View File

@ -50,6 +50,7 @@ export async function searchView(
result.rows.forEach(r => (r._viewId = view.id)) result.rows.forEach(r => (r._viewId = view.id))
ctx.body = result ctx.body = result
} }
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) { function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
if (request.sort) { if (request.sort) {
return { return {

View File

@ -1,4 +1,4 @@
import controller from "../../controllers/public/rows" import controller, { viewSearch } from "../../controllers/public/rows"
import Endpoint from "./utils/Endpoint" import Endpoint from "./utils/Endpoint"
import { externalSearchValidator } from "../utils/validators" import { externalSearchValidator } from "../utils/validators"
@ -200,7 +200,7 @@ read.push(
new Endpoint( new Endpoint(
"post", "post",
"/views/:viewId/rows/search", "/views/:viewId/rows/search",
controller.search controller.viewSearch
).addMiddleware(externalSearchValidator()) ).addMiddleware(externalSearchValidator())
) )

View File

@ -6,6 +6,7 @@ import {
ViewV2Schema, ViewV2Schema,
ViewV2, ViewV2,
ViewV2Type, ViewV2Type,
PublicAPIView,
} from "@budibase/types" } from "@budibase/types"
import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils" import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
@ -148,17 +149,17 @@ export class PublicViewAPI {
} }
async create( async create(
view: Omit<ViewV2, "id" | "version">, view: Omit<PublicAPIView, "id" | "version">,
expectations?: PublicAPIExpectations expectations?: PublicAPIExpectations
): Promise<Response<ViewV2>> { ): Promise<Response<PublicAPIView>> {
return this.request.send("post", "/views", view, expectations) return this.request.send("post", "/views", view, expectations)
} }
async update( async update(
viewId: string, viewId: string,
view: Omit<ViewV2, "id" | "version">, view: Omit<PublicAPIView, "id" | "version">,
expectations?: PublicAPIExpectations expectations?: PublicAPIExpectations
): Promise<Response<ViewV2>> { ): Promise<Response<PublicAPIView>> {
return this.request.send("put", `/views/${viewId}`, view, expectations) return this.request.send("put", `/views/${viewId}`, view, expectations)
} }
@ -177,20 +178,14 @@ export class PublicViewAPI {
async find( async find(
viewId: string, viewId: string,
expectations?: PublicAPIExpectations expectations?: PublicAPIExpectations
): Promise<Response<ViewV2>> { ): Promise<Response<PublicAPIView>> {
return this.request.send("get", `/views/${viewId}`, undefined, expectations) return this.request.send("get", `/views/${viewId}`, undefined, expectations)
} }
async fetch(
expectations?: PublicAPIExpectations
): Promise<Response<ViewV2[]>> {
return this.request.send("get", "/views", undefined, expectations)
}
async search( async search(
viewName: string, viewName: string,
expectations?: PublicAPIExpectations expectations?: PublicAPIExpectations
): Promise<Response<ViewV2[]>> { ): Promise<Response<PublicAPIView[]>> {
return this.request.send( return this.request.send(
"post", "post",
"/views/search", "/views/search",

View File

@ -1,6 +1,6 @@
import * as setup from "../../tests/utilities" import * as setup from "../../tests/utilities"
import { basicTable } from "../../../../tests/utilities/structures" import { basicTable } from "../../../../tests/utilities/structures"
import { Table } from "@budibase/types" import { BasicOperator, Table, UILogicalOperator } from "@budibase/types"
import { PublicAPIRequest } from "./Request" import { PublicAPIRequest } from "./Request"
import { generator } from "@budibase/backend-core/tests" 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 () => { it("should be able to update a view", async () => {
const view = await request.views.create(baseView(), { status: 201 }) const view = await request.views.create(baseView(), { status: 201 })
await request.views.update( const response = await request.views.update(view.data.id, {
view.data.id, ...view.data,
{ name: "new name",
...view.data, })
name: "new name",
},
{ status: 200 }
)
}) })
it("should be able to search views", async () => { it("should be able to search views", async () => {
@ -77,9 +73,18 @@ describe("check public API security", () => {
{ {
...baseView(), ...baseView(),
query: { query: {
string: { logicalOperator: UILogicalOperator.ANY,
name: "hello", groups: [
}, {
filters: [
{
operator: BasicOperator.STRING,
field: "name",
value: "hello",
},
],
},
],
}, },
}, },
{ status: 201 } { status: 201 }

View File

@ -12,6 +12,10 @@ import {
ViewV2Type, ViewV2Type,
SortOrder, SortOrder,
SortType, SortType,
UILogicalOperator,
BasicOperator,
ArrayOperator,
RangeOperator,
} from "@budibase/types" } from "@budibase/types"
import Joi, { CustomValidator } from "joi" import Joi, { CustomValidator } from "joi"
import { ValidSnippetNameRegex, helpers } from "@budibase/shared-core" 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() { export function viewValidator() {
return auth.joiValidator.body( return auth.joiValidator.body(
Joi.object({ Joi.object({
@ -78,7 +119,7 @@ export function viewValidator() {
type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION), type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION),
primaryDisplay: OPTIONAL_STRING, primaryDisplay: OPTIONAL_STRING,
schema: Joi.object().required(), schema: Joi.object().required(),
query: searchFiltersValidator().optional(), query: searchUIFilterValidator().optional(),
sort: Joi.object({ sort: Joi.object({
field: Joi.string().required(), field: Joi.string().required(),
order: Joi.string() order: Joi.string()