diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 2af95f444e..b8a407399c 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -1,14 +1,15 @@ import { quotas } from "@budibase/pro" import { UserCtx, - SearchRowResponse, - SortOrder, - SortType, ViewV2, + SearchRowRequest, + SearchRowResponse, } from "@budibase/types" import sdk from "../../../sdk" -export async function searchView(ctx: UserCtx) { +export async function searchView( + ctx: UserCtx +) { const { viewId } = ctx.params const view = await sdk.views.get(viewId) @@ -35,7 +36,7 @@ export async function searchView(ctx: UserCtx) { tableId: view.tableId, query: view.query || {}, fields: viewFields, - ...getSortOptions(ctx, view), + ...getSortOptions(ctx.request.body, view), }), { datasourceId: view.tableId, @@ -46,32 +47,12 @@ export async function searchView(ctx: UserCtx) { ctx.body = result } -function getSortOptions( - ctx: UserCtx, - view: ViewV2 -): - | { - sort: string - sortOrder?: SortOrder - sortType?: SortType - } - | undefined { - const { sort_column, sort_order, sort_type } = ctx.query - if (Array.isArray(sort_column)) { - ctx.throw(400, "sort_column cannot be an array") - } - if (Array.isArray(sort_order)) { - ctx.throw(400, "sort_order cannot be an array") - } - if (Array.isArray(sort_type)) { - ctx.throw(400, "sort_type cannot be an array") - } - - if (sort_column) { +function getSortOptions(request: SearchRowRequest, view: ViewV2) { + if (request.sort) { return { - sort: sort_column, - sortOrder: sort_order as SortOrder, - sortType: sort_type as SortType, + sort: request.sort, + sortOrder: request.sortOrder, + sortType: request.sortType, } } if (view.sort) { @@ -82,5 +63,5 @@ function getSortOptions( } } - return + return undefined } diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts index ac0cd2b4a4..cb1bab3361 100644 --- a/packages/server/src/api/routes/row.ts +++ b/packages/server/src/api/routes/row.ts @@ -269,7 +269,7 @@ router ) router - .get( + .post( "/api/v2/views/:viewId/search", authorized(PermissionType.VIEW, PermissionLevel.READ), rowController.views.searchView diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index b986ffb945..012ed537ee 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -813,252 +813,6 @@ describe("/rows", () => { }) }) - describe("view search", () => { - function userTable(): Table { - return { - name: "user", - type: "user", - schema: { - name: { - type: FieldType.STRING, - name: "name", - constraints: { type: "string" }, - }, - age: { - type: FieldType.NUMBER, - name: "age", - constraints: {}, - }, - }, - } - } - - it("returns table rows from view", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push(await config.createRow({ tableId: table._id })) - } - - const createViewResponse = await config.api.viewV2.create() - const response = await config.api.viewV2.search(createViewResponse.id) - - expect(response.body.rows).toHaveLength(10) - expect(response.body).toEqual({ - rows: expect.arrayContaining(rows.map(expect.objectContaining)), - }) - }) - - it("searching respects the view filters", async () => { - const table = await config.createTable(userTable()) - const expectedRows = [] - for (let i = 0; i < 10; i++) - await config.createRow({ - tableId: table._id, - name: generator.name(), - age: generator.integer({ min: 10, max: 30 }), - }) - - for (let i = 0; i < 5; i++) - expectedRows.push( - await config.createRow({ - tableId: table._id, - name: generator.name(), - age: 40, - }) - ) - - const createViewResponse = await config.api.viewV2.create({ - query: { equal: { age: 40 } }, - }) - - const response = await config.api.viewV2.search(createViewResponse.id) - - expect(response.body.rows).toHaveLength(5) - expect(response.body).toEqual({ - rows: expect.arrayContaining(expectedRows.map(expect.objectContaining)), - }) - }) - - const sortTestOptions: [ - { - field: string - order?: SortOrder - type?: SortType - }, - string[] - ][] = [ - [ - { - field: "name", - order: SortOrder.ASCENDING, - type: SortType.STRING, - }, - ["Alice", "Bob", "Charly", "Danny"], - ], - [ - { - field: "name", - }, - ["Alice", "Bob", "Charly", "Danny"], - ], - [ - { - field: "name", - order: SortOrder.DESCENDING, - }, - ["Danny", "Charly", "Bob", "Alice"], - ], - [ - { - field: "name", - order: SortOrder.DESCENDING, - type: SortType.STRING, - }, - ["Danny", "Charly", "Bob", "Alice"], - ], - [ - { - field: "age", - order: SortOrder.ASCENDING, - type: SortType.number, - }, - ["Danny", "Alice", "Charly", "Bob"], - ], - [ - { - field: "age", - order: SortOrder.ASCENDING, - }, - ["Danny", "Alice", "Charly", "Bob"], - ], - [ - { - field: "age", - order: SortOrder.DESCENDING, - }, - ["Bob", "Charly", "Alice", "Danny"], - ], - [ - { - field: "age", - order: SortOrder.DESCENDING, - type: SortType.number, - }, - ["Bob", "Charly", "Alice", "Danny"], - ], - ] - - it.each(sortTestOptions)( - "allow sorting (%s)", - async (sortParams, expected) => { - await config.createTable(userTable()) - const users = [ - { name: "Alice", age: 25 }, - { name: "Bob", age: 30 }, - { name: "Charly", age: 27 }, - { name: "Danny", age: 15 }, - ] - for (const user of users) { - await config.createRow({ - tableId: config.table!._id, - ...user, - }) - } - - const createViewResponse = await config.api.viewV2.create({ - sort: sortParams, - }) - - const response = await config.api.viewV2.search(createViewResponse.id) - - expect(response.body.rows).toHaveLength(4) - expect(response.body).toEqual({ - rows: expected.map(name => expect.objectContaining({ name })), - }) - } - ) - - it.each(sortTestOptions)( - "allow override the default view sorting (%s)", - async (sortParams, expected) => { - await config.createTable(userTable()) - const users = [ - { name: "Alice", age: 25 }, - { name: "Bob", age: 30 }, - { name: "Charly", age: 27 }, - { name: "Danny", age: 15 }, - ] - for (const user of users) { - await config.createRow({ - tableId: config.table!._id, - ...user, - }) - } - - const createViewResponse = await config.api.viewV2.create({ - sort: { - field: "name", - order: SortOrder.ASCENDING, - type: SortType.STRING, - }, - }) - - const response = await config.api.viewV2.search(createViewResponse.id, { - sort: { - column: sortParams.field, - order: sortParams.order, - type: sortParams.type, - }, - }) - - expect(response.body.rows).toHaveLength(4) - expect(response.body).toEqual({ - rows: expected.map(name => expect.objectContaining({ name })), - }) - } - ) - - it("when schema is defined, defined columns and row attributes are returned", async () => { - const table = await config.createTable(userTable()) - const rows = [] - for (let i = 0; i < 10; i++) { - rows.push( - await config.createRow({ - tableId: table._id, - name: generator.name(), - age: generator.age(), - }) - ) - } - - const view = await config.api.viewV2.create({ - schema: { name: {} }, - }) - const response = await config.api.viewV2.search(view.id) - - expect(response.body.rows).toHaveLength(10) - expect(response.body.rows).toEqual( - expect.arrayContaining( - rows.map(r => ({ - ...expectAnyInternalColsAttributes, - _viewId: view.id, - name: r.name, - })) - ) - ) - }) - - it("views without data can be returned", async () => { - const table = await config.createTable(userTable()) - - const createViewResponse = await config.api.viewV2.create() - const response = await config.api.viewV2.search(createViewResponse.id) - - expect(response.body.rows).toHaveLength(0) - }) - }) - describe("view 2.0", () => { function userTable(): Table { return { @@ -1261,5 +1015,256 @@ describe("/rows", () => { await config.api.row.get(tableId, rows[1]._id!, { expectStatus: 200 }) }) }) + + describe("view search", () => { + function userTable(): Table { + return { + name: "user", + type: "user", + schema: { + name: { + type: FieldType.STRING, + name: "name", + constraints: { type: "string" }, + }, + age: { + type: FieldType.NUMBER, + name: "age", + constraints: {}, + }, + }, + } + } + + it("returns table rows from view", async () => { + const table = await config.createTable(userTable()) + const rows = [] + for (let i = 0; i < 10; i++) { + rows.push(await config.createRow({ tableId: table._id })) + } + + const createViewResponse = await config.api.viewV2.create() + const response = await config.api.viewV2.search(createViewResponse.id) + + expect(response.body.rows).toHaveLength(10) + expect(response.body).toEqual({ + rows: expect.arrayContaining(rows.map(expect.objectContaining)), + }) + }) + + it("searching respects the view filters", async () => { + const table = await config.createTable(userTable()) + const expectedRows = [] + for (let i = 0; i < 10; i++) + await config.createRow({ + tableId: table._id, + name: generator.name(), + age: generator.integer({ min: 10, max: 30 }), + }) + + for (let i = 0; i < 5; i++) + expectedRows.push( + await config.createRow({ + tableId: table._id, + name: generator.name(), + age: 40, + }) + ) + + const createViewResponse = await config.api.viewV2.create({ + query: { equal: { age: 40 } }, + }) + + const response = await config.api.viewV2.search(createViewResponse.id) + + expect(response.body.rows).toHaveLength(5) + expect(response.body).toEqual({ + rows: expect.arrayContaining( + expectedRows.map(expect.objectContaining) + ), + }) + }) + + const sortTestOptions: [ + { + field: string + order?: SortOrder + type?: SortType + }, + string[] + ][] = [ + [ + { + field: "name", + order: SortOrder.ASCENDING, + type: SortType.STRING, + }, + ["Alice", "Bob", "Charly", "Danny"], + ], + [ + { + field: "name", + }, + ["Alice", "Bob", "Charly", "Danny"], + ], + [ + { + field: "name", + order: SortOrder.DESCENDING, + }, + ["Danny", "Charly", "Bob", "Alice"], + ], + [ + { + field: "name", + order: SortOrder.DESCENDING, + type: SortType.STRING, + }, + ["Danny", "Charly", "Bob", "Alice"], + ], + [ + { + field: "age", + order: SortOrder.ASCENDING, + type: SortType.number, + }, + ["Danny", "Alice", "Charly", "Bob"], + ], + [ + { + field: "age", + order: SortOrder.ASCENDING, + }, + ["Danny", "Alice", "Charly", "Bob"], + ], + [ + { + field: "age", + order: SortOrder.DESCENDING, + }, + ["Bob", "Charly", "Alice", "Danny"], + ], + [ + { + field: "age", + order: SortOrder.DESCENDING, + type: SortType.number, + }, + ["Bob", "Charly", "Alice", "Danny"], + ], + ] + + it.each(sortTestOptions)( + "allow sorting (%s)", + async (sortParams, expected) => { + await config.createTable(userTable()) + const users = [ + { name: "Alice", age: 25 }, + { name: "Bob", age: 30 }, + { name: "Charly", age: 27 }, + { name: "Danny", age: 15 }, + ] + for (const user of users) { + await config.createRow({ + tableId: config.table!._id, + ...user, + }) + } + + const createViewResponse = await config.api.viewV2.create({ + sort: sortParams, + }) + + const response = await config.api.viewV2.search(createViewResponse.id) + + expect(response.body.rows).toHaveLength(4) + expect(response.body).toEqual({ + rows: expected.map(name => expect.objectContaining({ name })), + }) + } + ) + + it.each(sortTestOptions)( + "allow override the default view sorting (%s)", + async (sortParams, expected) => { + await config.createTable(userTable()) + const users = [ + { name: "Alice", age: 25 }, + { name: "Bob", age: 30 }, + { name: "Charly", age: 27 }, + { name: "Danny", age: 15 }, + ] + for (const user of users) { + await config.createRow({ + tableId: config.table!._id, + ...user, + }) + } + + const createViewResponse = await config.api.viewV2.create({ + sort: { + field: "name", + order: SortOrder.ASCENDING, + type: SortType.STRING, + }, + }) + + const response = await config.api.viewV2.search( + createViewResponse.id, + { + sort: { + column: sortParams.field, + order: sortParams.order, + type: sortParams.type, + }, + } + ) + + expect(response.body.rows).toHaveLength(4) + expect(response.body).toEqual({ + rows: expected.map(name => expect.objectContaining({ name })), + }) + } + ) + + it("when schema is defined, defined columns and row attributes are returned", async () => { + const table = await config.createTable(userTable()) + const rows = [] + for (let i = 0; i < 10; i++) { + rows.push( + await config.createRow({ + tableId: table._id, + name: generator.name(), + age: generator.age(), + }) + ) + } + + const view = await config.api.viewV2.create({ + schema: { name: {} }, + }) + const response = await config.api.viewV2.search(view.id) + + expect(response.body.rows).toHaveLength(10) + expect(response.body.rows).toEqual( + expect.arrayContaining( + rows.map(r => ({ + ...expectAnyInternalColsAttributes, + _viewId: view.id, + name: r.name, + })) + ) + ) + }) + + it("views without data can be returned", async () => { + const table = await config.createTable(userTable()) + + const createViewResponse = await config.api.viewV2.create() + const response = await config.api.viewV2.search(createViewResponse.id) + + expect(response.body.rows).toHaveLength(0) + }) + }) }) }) diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 5ad2b2d3d7..0622fb823c 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -1,13 +1,12 @@ import { CreateViewRequest, - SortOrder, - SortType, UpdateViewRequest, DeleteRowRequest, PatchRowRequest, PatchRowResponse, Row, ViewV2, + SearchRequest, } from "@budibase/types" import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" @@ -81,31 +80,12 @@ export class ViewV2API extends TestAPI { search = async ( viewId: string, - options?: { - sort: { - column: string - order?: SortOrder - type?: SortType - } - }, + params?: SearchRequest, { expectStatus } = { expectStatus: 200 } ) => { - const qs: [string, any][] = [] - if (options?.sort.column) { - qs.push(["sort_column", options.sort.column]) - } - if (options?.sort.order) { - qs.push(["sort_order", options.sort.order]) - } - if (options?.sort.type) { - qs.push(["sort_type", options.sort.type]) - } - let url = `/api/v2/views/${viewId}/search` - if (qs.length) { - url += "?" + qs.map(q => q.join("=")).join("&") - } return this.request - .get(url) + .post(`/api/v2/views/${viewId}/search`) + .send(params) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) .expect(expectStatus)